Compare commits
4 Commits
77db46198a
...
89b0dcd784
| Author | SHA1 | Date | |
|---|---|---|---|
| 89b0dcd784 | |||
| e8464d3f74 | |||
| 8baafbdb0c | |||
| aebb5e6783 |
@@ -4,7 +4,10 @@
|
||||
"Bash(cat:*)",
|
||||
"Bash(cargo build:*)",
|
||||
"Bash(cargo test:*)",
|
||||
"Bash(cargo run:*)"
|
||||
"Bash(cargo run:*)",
|
||||
"Bash(cargo tree:*)",
|
||||
"WebFetch(domain:docs.rs)",
|
||||
"Bash(findstr:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
110
Cargo.lock
generated
110
Cargo.lock
generated
@@ -11,17 +11,33 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "atomic_refcell"
|
||||
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 = [
|
||||
"glam",
|
||||
"indexmap",
|
||||
"lru",
|
||||
"once_cell",
|
||||
"pretty_assertions",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
"sparsey",
|
||||
"thiserror",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -36,6 +52,12 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "glam"
|
||||
version = "0.29.3"
|
||||
@@ -45,6 +67,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
@@ -58,7 +91,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.16.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
@@ -69,12 +102,27 @@ version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.4.1"
|
||||
@@ -132,12 +180,27 @@ version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
@@ -181,6 +244,17 @@ dependencies = [
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sparsey"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb745f2600e2b85825349ae307cbddba8061afa1dc83f3901bf3cc546fc2c542"
|
||||
dependencies = [
|
||||
"atomic_refcell",
|
||||
"hashbrown 0.15.5",
|
||||
"rustc-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.111"
|
||||
@@ -224,6 +298,40 @@ version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
|
||||
12
Cargo.toml
12
Cargo.toml
@@ -31,6 +31,18 @@ regex = "1.10"
|
||||
# Math types (Vector2, Vector3, Quaternion, etc.)
|
||||
glam = { version = "0.29", features = ["serde"] }
|
||||
|
||||
# ECS (Entity Component System)
|
||||
sparsey = "0.13"
|
||||
|
||||
# LRU cache for reference resolution
|
||||
lru = "0.12"
|
||||
|
||||
# Directory traversal for loading projects
|
||||
walkdir = "2.4"
|
||||
|
||||
# Lazy static initialization for type registry
|
||||
once_cell = "1.19"
|
||||
|
||||
[dev-dependencies]
|
||||
# Testing utilities
|
||||
pretty_assertions = "1.4"
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
# Phase 1 Implementation Summary
|
||||
|
||||
## Overview
|
||||
|
||||
Phase 1 of the Cursebreaker Unity Parser has been successfully completed. The foundation for parsing Unity YAML files is now in place with a robust, well-tested implementation.
|
||||
|
||||
## What Was Implemented
|
||||
|
||||
### 1. Project Structure ✅
|
||||
|
||||
- Created Cargo workspace with proper dependencies
|
||||
- Set up module structure (lib.rs, error.rs, model/, parser/)
|
||||
- Configured Cargo.toml with metadata and feature flags
|
||||
|
||||
### 2. Error Handling ✅
|
||||
|
||||
- Implemented comprehensive error types using thiserror
|
||||
- Created custom error variants for:
|
||||
- IO errors
|
||||
- YAML parsing errors
|
||||
- Invalid Unity format
|
||||
- Missing headers
|
||||
- Invalid type tags
|
||||
- Reference errors
|
||||
- Result type alias for ergonomic error handling
|
||||
|
||||
### 3. Core Data Model ✅
|
||||
|
||||
**UnityFile:**
|
||||
- Represents a complete Unity file (.unity, .prefab, .asset)
|
||||
- Contains path and list of documents
|
||||
- Methods for querying documents:
|
||||
- `get_document(file_id)` - Look up by file ID
|
||||
- `get_documents_by_type(type_id)` - Find by Unity type ID
|
||||
- `get_documents_by_class(class_name)` - Find by class name
|
||||
|
||||
**UnityDocument:**
|
||||
- Represents a single YAML document (Unity object)
|
||||
- Contains:
|
||||
- `type_id` - Unity type ID (from !u!N tag)
|
||||
- `file_id` - Anchor ID (from &ID)
|
||||
- `class_name` - Object class (GameObject, Transform, etc.)
|
||||
- `properties` - Ordered map of properties (IndexMap)
|
||||
|
||||
### 4. YAML Document Parser ✅
|
||||
|
||||
**Features:**
|
||||
- Validates Unity YAML headers (%YAML 1.1, %TAG !u!)
|
||||
- Splits multi-document YAML files into individual documents
|
||||
- Handles empty lines and proper document boundaries
|
||||
- Parses YAML content into serde_yaml::Value structures
|
||||
- Stores properties in ordered IndexMap for stable iteration
|
||||
|
||||
**Implementation:**
|
||||
- `split_yaml_documents()` - Splits file on `---` boundaries
|
||||
- `validate_unity_header()` - Ensures proper Unity format
|
||||
- `parse_document()` - Converts raw YAML to UnityDocument
|
||||
|
||||
### 5. Unity Tag Parser ✅
|
||||
|
||||
**Features:**
|
||||
- Parses Unity type tags: `!u!1`, `!u!224`, etc.
|
||||
- Extracts type IDs and anchor IDs
|
||||
- Handles negative file IDs
|
||||
- Uses compiled regex with caching for performance
|
||||
|
||||
**Implementation:**
|
||||
- `parse_unity_tag()` - Extracts UnityTag from document string
|
||||
- Regex pattern: `^---\s+!u!(\d+)\s+&(-?\d+)`
|
||||
- OnceLock for one-time regex compilation
|
||||
|
||||
### 6. Testing Infrastructure ✅
|
||||
|
||||
**Test Coverage:**
|
||||
- **12 unit tests** - Parser components, YAML splitting, tag parsing
|
||||
- **7 integration tests** - Real Unity file parsing, error handling
|
||||
- **4 doc tests** - Documentation examples
|
||||
|
||||
**Real-World Testing:**
|
||||
- Successfully parses PiratePanic sample project files
|
||||
- Tests against actual Unity scenes and prefabs
|
||||
- Validates GameObject, Transform, and other Unity types
|
||||
|
||||
### 7. Documentation ✅
|
||||
|
||||
- Comprehensive rustdoc for all public APIs
|
||||
- Example code in `examples/basic_parsing.rs`
|
||||
- Updated README.md with usage guide
|
||||
- Updated ROADMAP.md with completed tasks
|
||||
- Implementation notes for future reference
|
||||
|
||||
## Files Created
|
||||
|
||||
```
|
||||
cursebreaker-parser-rust/
|
||||
├── Cargo.toml # Project configuration
|
||||
├── README.md # Project documentation
|
||||
├── PHASE1_SUMMARY.md # This file
|
||||
├── src/
|
||||
│ ├── lib.rs # Public API
|
||||
│ ├── error.rs # Error types
|
||||
│ ├── model/
|
||||
│ │ └── mod.rs # UnityFile, UnityDocument
|
||||
│ └── parser/
|
||||
│ ├── mod.rs # Main parser
|
||||
│ ├── unity_tag.rs # Unity tag parser
|
||||
│ └── yaml.rs # YAML document splitter
|
||||
├── examples/
|
||||
│ └── basic_parsing.rs # Usage example
|
||||
└── tests/
|
||||
└── integration_tests.rs # Integration tests
|
||||
```
|
||||
|
||||
## Key Metrics
|
||||
|
||||
- **Lines of Code**: ~800 (excluding tests)
|
||||
- **Test Coverage**: 23 tests, 100% pass rate
|
||||
- **Dependencies**: 6 main dependencies (minimal, well-maintained)
|
||||
- **Performance**:
|
||||
- Parse 15-doc prefab: ~1ms
|
||||
- Parse 100+ doc scene: ~10ms
|
||||
- Memory: ~2x file size
|
||||
|
||||
## Success Criteria Met ✅
|
||||
|
||||
All Phase 1 success criteria have been met:
|
||||
|
||||
1. ✅ Can read `Scene01MainMenu.unity` and split into individual documents
|
||||
2. ✅ Each document has correct type ID and file ID
|
||||
3. ✅ No panics on malformed input (returns errors)
|
||||
4. ✅ Successfully parses real Unity files from PiratePanic project
|
||||
5. ✅ Comprehensive test suite passing
|
||||
6. ✅ Clean, documented public API
|
||||
|
||||
## Next Steps
|
||||
|
||||
Phase 1 provides the foundation for more advanced features:
|
||||
|
||||
**Phase 2** (Next):
|
||||
- Property parsing and type conversion
|
||||
- Support for Unity-specific types (Vector3, Color, etc.)
|
||||
- Nested property access
|
||||
- GameObject and Component specialized types
|
||||
|
||||
**Future Phases**:
|
||||
- Reference resolution (Phase 3)
|
||||
- Performance optimization (Phase 4)
|
||||
- API polish and documentation (Phase 5)
|
||||
|
||||
## Usage Example
|
||||
|
||||
```rust
|
||||
use cursebreaker_parser::UnityFile;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Parse a Unity prefab
|
||||
let file = UnityFile::from_path("CardGrabber.prefab")?;
|
||||
|
||||
println!("Found {} documents", file.documents.len());
|
||||
|
||||
// Find all GameObjects
|
||||
let game_objects = file.get_documents_by_class("GameObject");
|
||||
println!("GameObjects: {}", game_objects.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
Phase 1 is complete and provides a solid foundation for the Cursebreaker Unity Parser. The implementation is:
|
||||
|
||||
- **Robust**: Comprehensive error handling
|
||||
- **Well-tested**: 23 passing tests
|
||||
- **Documented**: rustdoc for all public APIs
|
||||
- **Performant**: Fast parsing with minimal overhead
|
||||
- **Extensible**: Clean architecture for future phases
|
||||
|
||||
The parser successfully handles real Unity files and is ready for Phase 2 development.
|
||||
130
ROADMAP.md
130
ROADMAP.md
@@ -69,49 +69,49 @@ This roadmap breaks down the development into 5 phases, each building on the pre
|
||||
### Tasks
|
||||
|
||||
1. **Core Data Structures**
|
||||
- [ ] Implement `UnityDocument` struct
|
||||
- [ ] Implement `UnityFile` struct
|
||||
- [ ] Create property storage (PropertyMap using IndexMap)
|
||||
- [ ] Define FileID and LocalID types
|
||||
- [x] Implement `UnityDocument` struct
|
||||
- [x] Implement `UnityFile` struct
|
||||
- [x] Create property storage (PropertyMap using IndexMap)
|
||||
- [x] Define FileID and LocalID types
|
||||
|
||||
2. **Property Value Types**
|
||||
- [ ] Implement `PropertyValue` enum (Integer, Float, String, Boolean, etc.)
|
||||
- [ ] Add Vector3, Color, Quaternion value types
|
||||
- [ ] Add Array and nested Object support
|
||||
- [ ] Implement Debug and Display for PropertyValue
|
||||
- [x] Implement `PropertyValue` enum (Integer, Float, String, Boolean, etc.)
|
||||
- [x] Add Vector3, Color, Quaternion value types
|
||||
- [x] Add Array and nested Object support
|
||||
- [x] Implement Debug and Display for PropertyValue
|
||||
|
||||
3. **Property Parser**
|
||||
- [ ] Parse YAML mappings into PropertyMap
|
||||
- [ ] Handle nested properties (paths like `m_Component[0].component`)
|
||||
- [ ] Parse Unity-specific formats:
|
||||
- [ ] `{fileID: N}` references
|
||||
- [ ] `{x: 0, y: 0, z: 0}` vectors
|
||||
- [ ] `{r: 1, g: 1, b: 1, a: 1}` colors
|
||||
- [ ] `{guid: ..., type: N}` external references
|
||||
- [x] Parse YAML mappings into PropertyMap
|
||||
- [x] Handle nested properties (paths like `m_Component[0].component`)
|
||||
- [x] Parse Unity-specific formats:
|
||||
- [x] `{fileID: N}` references
|
||||
- [x] `{x: 0, y: 0, z: 0}` vectors
|
||||
- [x] `{r: 1, g: 1, b: 1, a: 1}` colors
|
||||
- [x] `{guid: ..., type: N}` external references
|
||||
|
||||
4. **GameObject & Component Models**
|
||||
- [ ] Create specialized GameObject struct
|
||||
- [ ] Create base Component trait/struct
|
||||
- [ ] Add common component types (Transform, RectTransform, etc.)
|
||||
- [ ] Helper methods for accessing common properties
|
||||
- [x] Create specialized GameObject struct
|
||||
- [x] Create base Component trait/struct
|
||||
- [x] Add common component types (Transform, RectTransform, etc.)
|
||||
- [x] Helper methods for accessing common properties
|
||||
|
||||
5. **Testing**
|
||||
- [ ] Unit tests for property parsing
|
||||
- [ ] Test all PropertyValue variants
|
||||
- [ ] Integration test: parse GameObject with components
|
||||
- [ ] Snapshot tests using sample Unity files
|
||||
- [x] Unit tests for property parsing
|
||||
- [x] Test all PropertyValue variants
|
||||
- [x] Integration test: parse GameObject with components
|
||||
- [x] Snapshot tests using sample Unity files
|
||||
|
||||
### Deliverables
|
||||
- [ ] ✓ Complete data model implemented
|
||||
- [ ] ✓ Properties parsed into type-safe structures
|
||||
- [ ] ✓ GameObject and Component abstractions working
|
||||
- [ ] ✓ All property types handled correctly
|
||||
- [x] ✓ Complete data model implemented
|
||||
- [x] ✓ Properties parsed into type-safe structures
|
||||
- [x] ✓ GameObject and Component abstractions working
|
||||
- [x] ✓ All property types handled correctly
|
||||
|
||||
### Success Criteria
|
||||
- [ ] Parse entire `CardGrabber.prefab` correctly
|
||||
- [ ] Extract all GameObject properties (name, components list)
|
||||
- [ ] Extract all Component properties with correct types
|
||||
- [ ] Can access nested properties programmatically
|
||||
- [x] Parse entire `CardGrabber.prefab` correctly
|
||||
- [x] Extract all GameObject properties (name, components list)
|
||||
- [x] Extract all Component properties with correct types
|
||||
- [x] Can access nested properties programmatically
|
||||
|
||||
---
|
||||
|
||||
@@ -122,54 +122,54 @@ This roadmap breaks down the development into 5 phases, each building on the pre
|
||||
### Tasks
|
||||
|
||||
1. **Reference Types**
|
||||
- [ ] Implement `FileReference` struct (fileID + optional GUID)
|
||||
- [ ] Implement `LocalReference` (within-file references)
|
||||
- [ ] Implement `ExternalReference` (cross-file GUID references)
|
||||
- [ ] Add reference equality and comparison
|
||||
- [x] Implement `FileReference` struct (fileID + optional GUID)
|
||||
- [x] Implement `LocalReference` (within-file references)
|
||||
- [x] Implement `ExternalReference` (cross-file GUID references)
|
||||
- [x] Add reference equality and comparison
|
||||
|
||||
2. **Type ID Mapping**
|
||||
- [ ] Create Unity type ID → class name mapping
|
||||
- [ ] Common types: GameObject(1), Transform(4), MonoBehaviour(114), etc.
|
||||
- [ ] Load type mappings from data file or hardcode common ones
|
||||
- [ ] Support unknown type IDs gracefully
|
||||
- [x] Create Unity type ID → class name mapping
|
||||
- [x] Common types: GameObject(1), Transform(4), MonoBehaviour(114), etc.
|
||||
- [x] Load type mappings from data file or hardcode common ones
|
||||
- [x] Support unknown type IDs gracefully
|
||||
|
||||
3. **Reference Resolution**
|
||||
- [ ] Implement within-file reference resolution
|
||||
- [ ] Cache resolved references for performance
|
||||
- [ ] Handle cyclic references safely
|
||||
- [ ] Detect and report broken references
|
||||
- [x] Implement within-file reference resolution
|
||||
- [x] Cache resolved references for performance
|
||||
- [x] Handle cyclic references safely
|
||||
- [x] Detect and report broken references
|
||||
|
||||
4. **UnityProject Multi-File Support**
|
||||
- [ ] Implement `UnityProject` struct
|
||||
- [ ] Load multiple Unity files into project
|
||||
- [ ] Build file ID → document index
|
||||
- [ ] Cross-file reference resolution (GUID-based)
|
||||
- [x] Implement `UnityProject` struct
|
||||
- [x] Load multiple Unity files into project
|
||||
- [x] Build file ID → document index
|
||||
- [x] Cross-file reference resolution (GUID-based)
|
||||
|
||||
5. **Query Helpers**
|
||||
- [ ] Find object by file ID
|
||||
- [ ] Find objects by type
|
||||
- [ ] Find objects by name
|
||||
- [ ] Get component from GameObject
|
||||
- [ ] Follow reference chains
|
||||
- [x] Find object by file ID
|
||||
- [x] Find objects by type
|
||||
- [x] Find objects by name
|
||||
- [x] Get component from GameObject
|
||||
- [x] Follow reference chains
|
||||
|
||||
6. **Testing**
|
||||
- [ ] Test reference resolution within single file
|
||||
- [ ] Test cross-file references (scene → prefab)
|
||||
- [ ] Test broken reference handling
|
||||
- [ ] Test circular reference detection
|
||||
- [x] Test reference resolution within single file
|
||||
- [x] Test cross-file references (scene → prefab)
|
||||
- [x] Test broken reference handling
|
||||
- [x] Test circular reference detection
|
||||
|
||||
### Deliverables
|
||||
- [ ] ✓ All references within files resolved correctly
|
||||
- [ ] ✓ Type ID system working with common Unity types
|
||||
- [ ] ✓ UnityProject can load and query multiple files
|
||||
- [ ] ✓ Query API functional
|
||||
- [x] ✓ All references within files resolved correctly
|
||||
- [x] ✓ Type ID system working with common Unity types
|
||||
- [x] ✓ UnityProject can load and query multiple files
|
||||
- [x] ✓ Query API functional
|
||||
|
||||
### Success Criteria
|
||||
- [ ] Load entire PiratePanic/Scenes/ directory
|
||||
- [ ] Resolve all GameObject → Component references
|
||||
- [ ] Resolve prefab references from scenes
|
||||
- [ ] Find objects by name across entire project
|
||||
- [ ] Handle missing references gracefully
|
||||
- [x] Load entire PiratePanic/Scenes/ directory
|
||||
- [x] Resolve all GameObject → Component references
|
||||
- [x] Resolve prefab references from scenes
|
||||
- [x] Find objects by name across entire project
|
||||
- [x] Handle missing references gracefully
|
||||
|
||||
---
|
||||
|
||||
|
||||
121
examples/guid_resolution.rs
Normal file
121
examples/guid_resolution.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
//! 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(())
|
||||
}
|
||||
352
src/ecs/mod.rs
Normal file
352
src/ecs/mod.rs
Normal file
@@ -0,0 +1,352 @@
|
||||
//! ECS world construction from Unity files
|
||||
//!
|
||||
//! This module provides functionality to build a Sparsey ECS World from
|
||||
//! parsed Unity files, creating entities for GameObjects and attaching
|
||||
//! components with resolved hierarchy.
|
||||
|
||||
use crate::{Error, FileID, GameObject, GenericComponent, RectTransform, Result, Transform, UnityProject};
|
||||
use sparsey::{Entity, World};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Component marker for GameObject data in the ECS world
|
||||
///
|
||||
/// This component holds the basic GameObject properties (name, active state, layer).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GameObjectComponent {
|
||||
/// The name of the GameObject
|
||||
pub name: String,
|
||||
/// Whether the GameObject is active
|
||||
pub is_active: bool,
|
||||
/// The layer the GameObject is on
|
||||
pub layer: i64,
|
||||
}
|
||||
|
||||
/// Builder for constructing an ECS World from a UnityProject
|
||||
///
|
||||
/// Uses a three-pass approach:
|
||||
/// 1. Create entities for all GameObjects
|
||||
/// 2. Attach components to entities
|
||||
/// 3. Resolve Transform hierarchy (parent/child relationships)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use cursebreaker_parser::{UnityProject, ecs::WorldBuilder};
|
||||
///
|
||||
/// let mut project = UnityProject::new(100);
|
||||
/// project.load_file("Assets/Scenes/MainMenu.unity")?;
|
||||
///
|
||||
/// let builder = WorldBuilder::new(project);
|
||||
/// let (world, entity_map) = builder.build()?;
|
||||
///
|
||||
/// println!("Created {} entities", entity_map.len());
|
||||
/// # Ok::<(), cursebreaker_parser::Error>(())
|
||||
/// ```
|
||||
pub struct WorldBuilder {
|
||||
project: UnityProject,
|
||||
file_id_to_entity: HashMap<FileID, Entity>,
|
||||
}
|
||||
|
||||
impl WorldBuilder {
|
||||
/// Create a new WorldBuilder with the given UnityProject
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `project` - The UnityProject to build from
|
||||
pub fn new(project: UnityProject) -> Self {
|
||||
Self {
|
||||
project,
|
||||
file_id_to_entity: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the ECS world
|
||||
///
|
||||
/// Returns the constructed World and a mapping of FileIDs to Entities.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Tuple of (World, FileID to Entity mapping)
|
||||
pub fn build(self) -> Result<(World, HashMap<FileID, Entity>)> {
|
||||
// TODO: Investigate correct Sparsey World creation API
|
||||
// The Sparsey API requires a GroupLayout parameter which needs investigation
|
||||
// For now, we return an error indicating ECS integration is incomplete
|
||||
return Err(Error::world_build_error(
|
||||
"Sparsey ECS integration not yet fully implemented - requires investigation of GroupLayout API"
|
||||
));
|
||||
|
||||
// Create World (placeholder - requires GroupLayout)
|
||||
// let mut world = World::new(&layout)?;
|
||||
|
||||
// Commented out due to Sparsey API investigation needed:
|
||||
/*
|
||||
// Pass 1: Create entities for all GameObjects
|
||||
for file in self.project.files().values() {
|
||||
for doc in &file.documents {
|
||||
if doc.is_game_object() {
|
||||
let entity = self.create_game_object_entity(&mut world, doc)?;
|
||||
self.file_id_to_entity.insert(doc.file_id, entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: Add components to entities
|
||||
for file in self.project.files().values() {
|
||||
for doc in &file.documents {
|
||||
if !doc.is_game_object() {
|
||||
self.add_component_to_entity(&mut world, doc)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 3: Resolve Transform hierarchy (parent/child relationships)
|
||||
self.resolve_transform_hierarchy(&mut world)?;
|
||||
|
||||
Ok((world, self.file_id_to_entity))
|
||||
*/
|
||||
}
|
||||
|
||||
/// Create an entity for a GameObject
|
||||
///
|
||||
/// Parses the GameObject and creates an entity with GameObjectComponent attached.
|
||||
#[allow(dead_code)]
|
||||
fn create_game_object_entity(
|
||||
&self,
|
||||
_world: &mut World,
|
||||
doc: &crate::UnityDocument,
|
||||
) -> Result<Entity> {
|
||||
let go = GameObject::parse(doc).ok_or_else(|| {
|
||||
Error::invalid_format("Failed to parse GameObject")
|
||||
})?;
|
||||
|
||||
let _component = GameObjectComponent {
|
||||
name: go.name().unwrap_or("Unnamed").to_string(),
|
||||
is_active: go.is_active(),
|
||||
layer: go.layer().unwrap_or(0),
|
||||
};
|
||||
|
||||
// TODO: Create entity using correct Sparsey API
|
||||
// let entity = world.spawn(...)?;
|
||||
// world.insert(entity, component)?;
|
||||
|
||||
Err(Error::world_build_error("Not implemented"))
|
||||
}
|
||||
|
||||
/// Add a component to an entity
|
||||
///
|
||||
/// Parses the component and attaches it to the appropriate GameObject entity.
|
||||
#[allow(dead_code)]
|
||||
fn add_component_to_entity(
|
||||
&mut self,
|
||||
_world: &mut World,
|
||||
doc: &crate::UnityDocument,
|
||||
) -> Result<()> {
|
||||
// Get the GameObject this component belongs to
|
||||
let game_object_ref = doc
|
||||
.get(&doc.class_name)
|
||||
.and_then(|v| v.as_object())
|
||||
.and_then(|obj| obj.get("m_GameObject"))
|
||||
.and_then(|v| v.as_file_ref());
|
||||
|
||||
let game_object_ref = match game_object_ref {
|
||||
Some(r) => r,
|
||||
None => {
|
||||
// Some components may not have m_GameObject (e.g., PrefabInstance)
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let entity = match self.file_id_to_entity.get(&game_object_ref.file_id) {
|
||||
Some(e) => *e,
|
||||
None => {
|
||||
// Log warning for missing GameObject reference (graceful degradation)
|
||||
eprintln!(
|
||||
"Warning: Component {} references unknown GameObject: {}",
|
||||
doc.file_id, game_object_ref.file_id
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// Parse and attach component based on type
|
||||
match doc.class_name.as_str() {
|
||||
"Transform" => {
|
||||
if let Some(_transform) = Transform::parse(doc) {
|
||||
// Store the transform entity mapping for hierarchy resolution
|
||||
self.file_id_to_entity.insert(doc.file_id, entity);
|
||||
|
||||
// Note: Actual component attachment depends on Sparsey API
|
||||
// world.insert(entity, transform)?;
|
||||
}
|
||||
}
|
||||
"RectTransform" => {
|
||||
if let Some(_rect_transform) = RectTransform::parse(doc) {
|
||||
self.file_id_to_entity.insert(doc.file_id, entity);
|
||||
|
||||
// Note: Actual component attachment depends on Sparsey API
|
||||
// world.insert(entity, rect_transform)?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if let Some(_generic_component) = GenericComponent::parse(doc) {
|
||||
// Note: Actual component attachment depends on Sparsey API
|
||||
// world.insert(entity, generic_component)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve Transform hierarchy (parent/child relationships)
|
||||
///
|
||||
/// Iterates through all Transform/RectTransform components and sets up
|
||||
/// the parent/child Entity references.
|
||||
#[allow(dead_code)]
|
||||
fn resolve_transform_hierarchy(&mut self, _world: &mut World) -> Result<()> {
|
||||
// Collect all transform documents first to avoid borrow checker issues
|
||||
let mut transform_updates: Vec<(Entity, Option<Entity>, Vec<Entity>)> = Vec::new();
|
||||
|
||||
for file in self.project.files().values() {
|
||||
for doc in &file.documents {
|
||||
if doc.is_transform() {
|
||||
let transform_entity = match self.file_id_to_entity.get(&doc.file_id) {
|
||||
Some(e) => *e,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// Get parent reference
|
||||
let parent_ref = doc
|
||||
.get(&doc.class_name)
|
||||
.and_then(|v| v.as_object())
|
||||
.and_then(|obj| obj.get("m_Father"))
|
||||
.and_then(|v| v.as_file_ref());
|
||||
|
||||
let parent_entity = parent_ref
|
||||
.and_then(|r| {
|
||||
if r.file_id.as_i64() == 0 {
|
||||
None // Null reference
|
||||
} else {
|
||||
self.file_id_to_entity.get(&r.file_id).copied()
|
||||
}
|
||||
});
|
||||
|
||||
// Get children references
|
||||
let children_refs = doc
|
||||
.get(&doc.class_name)
|
||||
.and_then(|v| v.as_object())
|
||||
.and_then(|obj| obj.get("m_Children"))
|
||||
.and_then(|v| v.as_array());
|
||||
|
||||
let children_entities: Vec<Entity> = children_refs
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|child| {
|
||||
child
|
||||
.as_file_ref()
|
||||
.and_then(|r| self.file_id_to_entity.get(&r.file_id).copied())
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
transform_updates.push((transform_entity, parent_entity, children_entities));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Apply hierarchy updates to Transform components in the world
|
||||
// This requires mutable access to the Transform components
|
||||
// Actual implementation depends on Sparsey's component access API
|
||||
//
|
||||
// Example pseudo-code:
|
||||
// for (entity, parent, children) in transform_updates {
|
||||
// if let Some(mut transform) = world.get_mut::<Transform>(entity) {
|
||||
// transform.set_parent(parent);
|
||||
// transform.set_children(children);
|
||||
// }
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a Sparsey World from a UnityProject
|
||||
///
|
||||
/// Convenience function that creates a WorldBuilder and builds the world.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `project` - The UnityProject to build from
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Tuple of (World, FileID to Entity mapping)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use cursebreaker_parser::{UnityProject, ecs::build_world_from_project};
|
||||
///
|
||||
/// let mut project = UnityProject::new(100);
|
||||
/// project.load_file("Assets/Scenes/MainMenu.unity")?;
|
||||
///
|
||||
/// let (world, entity_map) = build_world_from_project(project)?;
|
||||
/// println!("Created {} entities", entity_map.len());
|
||||
/// # Ok::<(), cursebreaker_parser::Error>(())
|
||||
/// ```
|
||||
pub fn build_world_from_project(project: UnityProject) -> Result<(World, HashMap<FileID, Entity>)> {
|
||||
let builder = WorldBuilder::new(project);
|
||||
builder.build()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn test_build_world_from_prefab() {
|
||||
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();
|
||||
|
||||
let result = build_world_from_project(project);
|
||||
|
||||
// TODO: Remove this assertion once Sparsey integration is complete
|
||||
// For now, we expect an error since Sparsey API needs investigation
|
||||
assert!(result.is_err(), "Expected error due to incomplete Sparsey integration");
|
||||
|
||||
if let Err(Error::WorldBuildError(_)) = result {
|
||||
// Expected error
|
||||
} else {
|
||||
panic!("Expected WorldBuildError");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_world_builder() {
|
||||
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();
|
||||
|
||||
let builder = WorldBuilder::new(project);
|
||||
let result = builder.build();
|
||||
|
||||
// TODO: Remove this assertion once Sparsey integration is complete
|
||||
// For now, we expect an error since Sparsey API needs investigation
|
||||
assert!(result.is_err(), "Expected error due to incomplete Sparsey integration");
|
||||
|
||||
if let Err(Error::WorldBuildError(_)) = result {
|
||||
// Expected error
|
||||
} else {
|
||||
panic!("Expected WorldBuildError");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/error.rs
36
src/error.rs
@@ -54,6 +54,22 @@ pub enum Error {
|
||||
/// Invalid property path
|
||||
#[error("Invalid property path: {0}")]
|
||||
InvalidPropertyPath(String),
|
||||
|
||||
/// Failed to resolve GUID reference
|
||||
#[error("Failed to resolve GUID reference: {0}")]
|
||||
GuidResolutionError(String),
|
||||
|
||||
/// Unknown Unity type ID
|
||||
#[error("Unknown Unity type ID: {0} (this type is not in the registry)")]
|
||||
UnknownTypeId(u32),
|
||||
|
||||
/// Circular reference detected
|
||||
#[error("Circular reference detected in reference chain")]
|
||||
CircularReference,
|
||||
|
||||
/// ECS world construction error
|
||||
#[error("Failed to build ECS world: {0}")]
|
||||
WorldBuildError(String),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
@@ -71,4 +87,24 @@ impl Error {
|
||||
pub fn property_not_found(msg: impl Into<String>) -> Self {
|
||||
Error::PropertyNotFound(msg.into())
|
||||
}
|
||||
|
||||
/// Create a GUID resolution error
|
||||
pub fn guid_resolution_error(msg: impl Into<String>) -> Self {
|
||||
Error::GuidResolutionError(msg.into())
|
||||
}
|
||||
|
||||
/// Create an unknown type ID error
|
||||
pub fn unknown_type_id(type_id: u32) -> Self {
|
||||
Error::UnknownTypeId(type_id)
|
||||
}
|
||||
|
||||
/// Create a circular reference error
|
||||
pub fn circular_reference() -> Self {
|
||||
Error::CircularReference
|
||||
}
|
||||
|
||||
/// Create a world build error
|
||||
pub fn world_build_error(msg: impl Into<String>) -> Self {
|
||||
Error::WorldBuildError(msg.into())
|
||||
}
|
||||
}
|
||||
|
||||
11
src/lib.rs
11
src/lib.rs
@@ -16,18 +16,23 @@
|
||||
//! ```
|
||||
|
||||
// Public modules
|
||||
pub mod ecs;
|
||||
pub mod error;
|
||||
pub mod model;
|
||||
pub mod parser;
|
||||
pub mod project;
|
||||
pub mod property;
|
||||
pub mod types;
|
||||
|
||||
// Re-exports
|
||||
pub use ecs::{build_world_from_project, GameObjectComponent, WorldBuilder};
|
||||
pub use error::{Error, Result};
|
||||
pub use model::{UnityDocument, UnityFile};
|
||||
pub use parser::parse_unity_file;
|
||||
pub use parser::{meta::MetaFile, parse_unity_file};
|
||||
pub use project::UnityProject;
|
||||
pub use property::PropertyValue;
|
||||
pub use types::{
|
||||
Color, Component, ExternalRef, FileID, FileRef, GameObject, GenericComponent, LocalID,
|
||||
Quaternion, RectTransform, Transform, Vector2, Vector3,
|
||||
get_class_name, get_type_id, Color, Component, ExternalRef, FileID, FileRef, GameObject,
|
||||
GenericComponent, LocalID, Quaternion, RectTransform, Transform, UnityReference, Vector2,
|
||||
Vector3,
|
||||
};
|
||||
|
||||
213
src/parser/meta.rs
Normal file
213
src/parser/meta.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
//! Unity .meta file parser
|
||||
//!
|
||||
//! Unity creates .meta files alongside assets to store metadata including
|
||||
//! the unique GUID that identifies each asset.
|
||||
|
||||
use crate::{Error, Result};
|
||||
use std::path::Path;
|
||||
|
||||
/// Represents a Unity .meta file
|
||||
///
|
||||
/// .meta files contain metadata about Unity assets, most importantly
|
||||
/// the GUID which uniquely identifies the asset across the project.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct MetaFile {
|
||||
/// The unique GUID for this asset
|
||||
pub guid: String,
|
||||
|
||||
/// The file format version
|
||||
pub file_format_version: Option<i64>,
|
||||
}
|
||||
|
||||
impl MetaFile {
|
||||
/// Parse a Unity .meta file from the given path
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Path to the .meta file
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use cursebreaker_parser::parser::meta::MetaFile;
|
||||
///
|
||||
/// let meta = MetaFile::from_path("Assets/Prefabs/Player.prefab.meta")?;
|
||||
/// println!("GUID: {}", meta.guid);
|
||||
/// # Ok::<(), cursebreaker_parser::Error>(())
|
||||
/// ```
|
||||
pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
|
||||
let path = path.as_ref();
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
Self::from_str(&content)
|
||||
}
|
||||
|
||||
/// Parse a Unity .meta file from a string
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `content` - The YAML content of the .meta file
|
||||
pub fn from_str(content: &str) -> Result<Self> {
|
||||
// Parse as YAML
|
||||
let yaml: serde_yaml::Value = serde_yaml::from_str(content)?;
|
||||
|
||||
// Extract GUID (required field)
|
||||
let guid = yaml
|
||||
.get("guid")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| Error::invalid_format("Missing 'guid' field in .meta file"))?
|
||||
.to_string();
|
||||
|
||||
// Extract file format version (optional)
|
||||
let file_format_version = yaml
|
||||
.get("fileFormatVersion")
|
||||
.and_then(|v| v.as_i64());
|
||||
|
||||
Ok(Self {
|
||||
guid,
|
||||
file_format_version,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the GUID from this .meta file
|
||||
pub fn guid(&self) -> &str {
|
||||
&self.guid
|
||||
}
|
||||
|
||||
/// Get the file format version
|
||||
pub fn file_format_version(&self) -> Option<i64> {
|
||||
self.file_format_version
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the .meta file path for a given asset path
|
||||
///
|
||||
/// Unity creates .meta files alongside assets with the .meta extension.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `asset_path` - Path to the Unity asset
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use cursebreaker_parser::parser::meta::get_meta_path;
|
||||
/// use std::path::PathBuf;
|
||||
///
|
||||
/// let asset = PathBuf::from("Assets/Scenes/MainMenu.unity");
|
||||
/// let meta = get_meta_path(&asset);
|
||||
/// assert_eq!(meta, PathBuf::from("Assets/Scenes/MainMenu.unity.meta"));
|
||||
/// ```
|
||||
pub fn get_meta_path(asset_path: &Path) -> std::path::PathBuf {
|
||||
let mut meta_path = asset_path.to_path_buf();
|
||||
let mut filename = meta_path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_os_string();
|
||||
filename.push(".meta");
|
||||
meta_path.set_file_name(filename);
|
||||
meta_path
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn test_parse_meta_file() {
|
||||
let content = r#"
|
||||
fileFormatVersion: 2
|
||||
guid: 4ab6bfb0ff54cdf4c8dd38ca244d6f15
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
"#;
|
||||
|
||||
let meta = MetaFile::from_str(content).unwrap();
|
||||
assert_eq!(meta.guid(), "4ab6bfb0ff54cdf4c8dd38ca244d6f15");
|
||||
assert_eq!(meta.file_format_version(), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_meta_file_minimal() {
|
||||
let content = r#"
|
||||
guid: abc123def456
|
||||
"#;
|
||||
|
||||
let meta = MetaFile::from_str(content).unwrap();
|
||||
assert_eq!(meta.guid(), "abc123def456");
|
||||
assert_eq!(meta.file_format_version(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_meta_file_missing_guid() {
|
||||
let content = r#"
|
||||
fileFormatVersion: 2
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
"#;
|
||||
|
||||
let result = MetaFile::from_str(content);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_meta_path() {
|
||||
let asset = PathBuf::from("Assets/Scenes/MainMenu.unity");
|
||||
let meta = get_meta_path(&asset);
|
||||
assert_eq!(meta, PathBuf::from("Assets/Scenes/MainMenu.unity.meta"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_meta_path_prefab() {
|
||||
let asset = PathBuf::from("Assets/Prefabs/Player.prefab");
|
||||
let meta = get_meta_path(&asset);
|
||||
assert_eq!(meta, PathBuf::from("Assets/Prefabs/Player.prefab.meta"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_meta_path_asset() {
|
||||
let asset = PathBuf::from("Assets/ScriptableObjects/Config.asset");
|
||||
let meta = get_meta_path(&asset);
|
||||
assert_eq!(meta, PathBuf::from("Assets/ScriptableObjects/Config.asset.meta"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_real_meta_file() {
|
||||
// This tests with a realistic Unity .meta file structure
|
||||
let content = r#"
|
||||
fileFormatVersion: 2
|
||||
guid: 06560ff19ed0c0b43918260dee8775dd
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
"#;
|
||||
|
||||
let meta = MetaFile::from_str(content).unwrap();
|
||||
assert_eq!(meta.guid(), "06560ff19ed0c0b43918260dee8775dd");
|
||||
assert_eq!(meta.file_format_version(), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_script_meta_file() {
|
||||
let content = r#"
|
||||
fileFormatVersion: 2
|
||||
guid: b12cf7f429956b944a0d0e4b85516679
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
"#;
|
||||
|
||||
let meta = MetaFile::from_str(content).unwrap();
|
||||
assert_eq!(meta.guid(), "b12cf7f429956b944a0d0e4b85516679");
|
||||
assert_eq!(meta.file_format_version(), Some(2));
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
//! Unity YAML parsing module
|
||||
|
||||
pub mod meta;
|
||||
mod unity_tag;
|
||||
mod yaml;
|
||||
|
||||
pub use meta::{MetaFile, get_meta_path};
|
||||
pub use unity_tag::{UnityTag, parse_unity_tag};
|
||||
pub use yaml::split_yaml_documents;
|
||||
|
||||
|
||||
512
src/project/mod.rs
Normal file
512
src/project/mod.rs
Normal file
@@ -0,0 +1,512 @@
|
||||
//! 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
280
src/project/query.rs
Normal file
280
src/project/query.rs
Normal file
@@ -0,0 +1,280 @@
|
||||
//! Query helper methods for UnityProject
|
||||
//!
|
||||
//! This module extends UnityProject with convenient query methods
|
||||
//! for finding and filtering Unity objects.
|
||||
|
||||
use crate::{UnityDocument, UnityProject};
|
||||
|
||||
impl UnityProject {
|
||||
/// Find all documents of a specific type across all files
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `type_id` - Unity type ID to search for
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use cursebreaker_parser::UnityProject;
|
||||
///
|
||||
/// let project = UnityProject::new(100);
|
||||
/// let game_objects = project.find_all_by_type(1); // GameObject = type 1
|
||||
/// println!("Found {} GameObjects", game_objects.len());
|
||||
/// ```
|
||||
pub fn find_all_by_type(&self, type_id: u32) -> Vec<&UnityDocument> {
|
||||
self.files
|
||||
.values()
|
||||
.flat_map(|file| file.get_documents_by_type(type_id))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Find all documents with a specific class name across all files
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `class_name` - Unity class name to search for (e.g., "GameObject", "Transform")
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use cursebreaker_parser::UnityProject;
|
||||
///
|
||||
/// let project = UnityProject::new(100);
|
||||
/// let transforms = project.find_all_by_class("Transform");
|
||||
/// println!("Found {} Transforms", transforms.len());
|
||||
/// ```
|
||||
pub fn find_all_by_class(&self, class_name: &str) -> Vec<&UnityDocument> {
|
||||
self.files
|
||||
.values()
|
||||
.flat_map(|file| file.get_documents_by_class(class_name))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Find documents by name (searches for m_Name property)
|
||||
///
|
||||
/// Searches for GameObjects with a specific m_Name value.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name to search for
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use cursebreaker_parser::UnityProject;
|
||||
///
|
||||
/// let project = UnityProject::new(100);
|
||||
/// let players = project.find_by_name("Player");
|
||||
/// for player in players {
|
||||
/// println!("Found Player at FileID: {}", player.file_id);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn find_by_name(&self, name: &str) -> Vec<&UnityDocument> {
|
||||
self.files
|
||||
.values()
|
||||
.flat_map(|file| &file.documents)
|
||||
.filter(|doc| {
|
||||
// Check for m_Name in the root object
|
||||
if let Some(obj) = doc.get(&doc.class_name).and_then(|v| v.as_object()) {
|
||||
if let Some(doc_name) = obj.get("m_Name").and_then(|v| v.as_str()) {
|
||||
return doc_name == name;
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get a component from a GameObject by component type
|
||||
///
|
||||
/// Searches the GameObject's m_Component array for a component with the specified class name.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `game_object` - The GameObject document
|
||||
/// * `component_type` - The component class name to find (e.g., "Transform", "SpriteRenderer")
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use cursebreaker_parser::UnityProject;
|
||||
///
|
||||
/// let project = UnityProject::new(100);
|
||||
/// # let game_object = &project.find_all_by_type(1)[0];
|
||||
/// if let Some(transform) = project.get_component(game_object, "Transform") {
|
||||
/// println!("Found Transform component");
|
||||
/// }
|
||||
/// ```
|
||||
pub fn get_component(
|
||||
&self,
|
||||
game_object: &UnityDocument,
|
||||
component_type: &str,
|
||||
) -> Option<&UnityDocument> {
|
||||
if !game_object.is_game_object() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Get the m_Component array
|
||||
let components = game_object
|
||||
.get("GameObject")
|
||||
.and_then(|v| v.as_object())?
|
||||
.get("m_Component")
|
||||
.and_then(|v| v.as_array())?;
|
||||
|
||||
// Search for the component type
|
||||
for comp_entry in components {
|
||||
if let Some(obj) = comp_entry.as_object() {
|
||||
if let Some(comp_ref) = obj.get("component").and_then(|v| v.as_file_ref()) {
|
||||
if let Some(comp_doc) = self.get_document(comp_ref.file_id) {
|
||||
if comp_doc.class_name == component_type {
|
||||
return Some(comp_doc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Get all components from a GameObject
|
||||
///
|
||||
/// Returns all components attached to the specified GameObject.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `game_object` - The GameObject document
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use cursebreaker_parser::UnityProject;
|
||||
///
|
||||
/// let project = UnityProject::new(100);
|
||||
/// # let game_object = &project.find_all_by_type(1)[0];
|
||||
/// let components = project.get_all_components(game_object);
|
||||
/// for component in components {
|
||||
/// println!("Component: {}", component.class_name);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn get_all_components(&self, game_object: &UnityDocument) -> Vec<&UnityDocument> {
|
||||
if !game_object.is_game_object() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let components = match game_object
|
||||
.get("GameObject")
|
||||
.and_then(|v| v.as_object())
|
||||
.and_then(|obj| obj.get("m_Component"))
|
||||
.and_then(|v| v.as_array())
|
||||
{
|
||||
Some(c) => c,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
|
||||
components
|
||||
.iter()
|
||||
.filter_map(|comp_entry| {
|
||||
comp_entry
|
||||
.as_object()?
|
||||
.get("component")
|
||||
.and_then(|v| v.as_file_ref())
|
||||
.and_then(|r| self.get_document(r.file_id))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn test_find_all_by_type() {
|
||||
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();
|
||||
|
||||
// Find all GameObjects (type 1)
|
||||
let game_objects = project.find_all_by_type(1);
|
||||
assert!(game_objects.len() > 0, "Should find at least one GameObject");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_all_by_class() {
|
||||
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();
|
||||
|
||||
// Find all Transform or RectTransform components (UI prefabs typically use RectTransform)
|
||||
let transforms = project.find_all_by_class("Transform");
|
||||
let rect_transforms = project.find_all_by_class("RectTransform");
|
||||
|
||||
assert!(
|
||||
transforms.len() > 0 || rect_transforms.len() > 0,
|
||||
"Should find at least one Transform or RectTransform"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_by_name() {
|
||||
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();
|
||||
|
||||
// The prefab should have a root GameObject with the name "CardGrabber"
|
||||
let results = project.find_by_name("CardGrabber");
|
||||
assert!(results.len() > 0, "Should find GameObject named 'CardGrabber'");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_component() {
|
||||
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();
|
||||
|
||||
// Find a GameObject
|
||||
let game_objects = project.find_all_by_type(1);
|
||||
if let Some(go) = game_objects.first() {
|
||||
// Every GameObject should have a Transform
|
||||
let transform = project.get_component(go, "Transform");
|
||||
assert!(transform.is_some() || project.get_component(go, "RectTransform").is_some(),
|
||||
"GameObject should have Transform or RectTransform component");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_all_components() {
|
||||
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();
|
||||
|
||||
// Find a GameObject
|
||||
let game_objects = project.find_all_by_type(1);
|
||||
if let Some(go) = game_objects.first() {
|
||||
let components = project.get_all_components(go);
|
||||
assert!(components.len() > 0, "GameObject should have at least one component");
|
||||
|
||||
// Verify all returned items are actual components
|
||||
for component in components {
|
||||
assert!(!component.is_game_object(), "Should not return GameObject as component");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Component trait and generic component wrapper
|
||||
|
||||
use crate::model::UnityDocument;
|
||||
use crate::types::FileID;
|
||||
use crate::types::FileRef;
|
||||
|
||||
/// A trait for Unity components
|
||||
@@ -15,8 +16,8 @@ pub trait Component {
|
||||
true // Default implementation
|
||||
}
|
||||
|
||||
/// Get the underlying UnityDocument
|
||||
fn document(&self) -> &UnityDocument;
|
||||
/// Get the file ID of this component
|
||||
fn file_id(&self) -> FileID;
|
||||
}
|
||||
|
||||
/// A generic component wrapper that works with any component type
|
||||
@@ -29,8 +30,8 @@ pub trait Component {
|
||||
/// let file = UnityFile::from_path("Scene.unity")?;
|
||||
/// for doc in &file.documents {
|
||||
/// if !doc.is_game_object() {
|
||||
/// if let Some(comp) = GenericComponent::new(doc) {
|
||||
/// println!("Component: {}", doc.class_name);
|
||||
/// if let Some(comp) = GenericComponent::parse(doc) {
|
||||
/// println!("Component: {}", comp.class_name());
|
||||
/// if let Some(go_ref) = comp.game_object() {
|
||||
/// println!(" Attached to: {}", go_ref.file_id);
|
||||
/// }
|
||||
@@ -39,52 +40,63 @@ pub trait Component {
|
||||
/// }
|
||||
/// # Ok::<(), cursebreaker_parser::Error>(())
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct GenericComponent<'a> {
|
||||
document: &'a UnityDocument,
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GenericComponent {
|
||||
file_id: FileID,
|
||||
class_name: String,
|
||||
game_object: Option<FileRef>,
|
||||
is_enabled: bool,
|
||||
}
|
||||
|
||||
impl<'a> GenericComponent<'a> {
|
||||
/// Create a GenericComponent wrapper from a UnityDocument
|
||||
impl GenericComponent {
|
||||
/// Parse a GenericComponent from a UnityDocument
|
||||
///
|
||||
/// Returns None if the document is a GameObject (GameObjects are not components).
|
||||
pub fn new(document: &'a UnityDocument) -> Option<Self> {
|
||||
if !document.is_game_object() {
|
||||
Some(Self { document })
|
||||
} else {
|
||||
None
|
||||
pub fn parse(document: &UnityDocument) -> Option<Self> {
|
||||
if document.is_game_object() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Extract m_GameObject and m_Enabled from the component properties
|
||||
let props = document
|
||||
.get(&document.class_name)
|
||||
.and_then(|obj| obj.as_object());
|
||||
|
||||
let game_object = props
|
||||
.and_then(|p| p.get("m_GameObject"))
|
||||
.and_then(|v| v.as_file_ref())
|
||||
.copied();
|
||||
|
||||
let is_enabled = props
|
||||
.and_then(|p| p.get("m_Enabled"))
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(true);
|
||||
|
||||
Some(Self {
|
||||
file_id: document.file_id,
|
||||
class_name: document.class_name.clone(),
|
||||
game_object,
|
||||
is_enabled,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the class name of this component
|
||||
pub fn class_name(&self) -> &str {
|
||||
&self.document.class_name
|
||||
&self.class_name
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Component for GenericComponent<'a> {
|
||||
impl Component for GenericComponent {
|
||||
fn game_object(&self) -> Option<FileRef> {
|
||||
// Look for m_GameObject property which is common to all components
|
||||
self.document
|
||||
.get(&self.document.class_name)
|
||||
.and_then(|obj| obj.as_object())
|
||||
.and_then(|props| props.get("m_GameObject"))
|
||||
.and_then(|v| v.as_file_ref())
|
||||
.copied()
|
||||
self.game_object
|
||||
}
|
||||
|
||||
fn is_enabled(&self) -> bool {
|
||||
// Look for m_Enabled property
|
||||
self.document
|
||||
.get(&self.document.class_name)
|
||||
.and_then(|obj| obj.as_object())
|
||||
.and_then(|props| props.get("m_Enabled"))
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(true) // Default to enabled
|
||||
self.is_enabled
|
||||
}
|
||||
|
||||
fn document(&self) -> &UnityDocument {
|
||||
self.document
|
||||
fn file_id(&self) -> FileID {
|
||||
self.file_id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,14 +130,14 @@ mod tests {
|
||||
#[test]
|
||||
fn test_component_creation() {
|
||||
let doc = create_test_component();
|
||||
let comp = GenericComponent::new(&doc);
|
||||
let comp = GenericComponent::parse(&doc);
|
||||
assert!(comp.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_component_game_object_ref() {
|
||||
let doc = create_test_component();
|
||||
let comp = GenericComponent::new(&doc).unwrap();
|
||||
let comp = GenericComponent::parse(&doc).unwrap();
|
||||
let go_ref = comp.game_object();
|
||||
assert!(go_ref.is_some());
|
||||
assert_eq!(go_ref.unwrap().file_id.as_i64(), 67890);
|
||||
@@ -134,17 +146,24 @@ mod tests {
|
||||
#[test]
|
||||
fn test_component_is_enabled() {
|
||||
let doc = create_test_component();
|
||||
let comp = GenericComponent::new(&doc).unwrap();
|
||||
let comp = GenericComponent::parse(&doc).unwrap();
|
||||
assert!(comp.is_enabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_component_class_name() {
|
||||
let doc = create_test_component();
|
||||
let comp = GenericComponent::new(&doc).unwrap();
|
||||
let comp = GenericComponent::parse(&doc).unwrap();
|
||||
assert_eq!(comp.class_name(), "Transform");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_component_file_id() {
|
||||
let doc = create_test_component();
|
||||
let comp = GenericComponent::parse(&doc).unwrap();
|
||||
assert_eq!(comp.file_id().as_i64(), 12345);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_game_object_is_not_component() {
|
||||
let mut properties = IndexMap::new();
|
||||
@@ -157,7 +176,7 @@ mod tests {
|
||||
properties,
|
||||
};
|
||||
|
||||
let comp = GenericComponent::new(&doc);
|
||||
let comp = GenericComponent::parse(&doc);
|
||||
assert!(comp.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! GameObject wrapper for ergonomic access to GameObject properties
|
||||
|
||||
use crate::model::UnityDocument;
|
||||
use crate::types::{FileID, FileRef};
|
||||
|
||||
/// A wrapper around a UnityDocument that represents a GameObject
|
||||
///
|
||||
@@ -14,104 +13,88 @@ use crate::types::{FileID, FileRef};
|
||||
///
|
||||
/// let file = UnityFile::from_path("Scene.unity")?;
|
||||
/// for doc in &file.documents {
|
||||
/// if let Some(go) = GameObject::new(doc) {
|
||||
/// if let Some(go) = GameObject::parse(doc) {
|
||||
/// println!("GameObject: {}", go.name().unwrap_or("Unnamed"));
|
||||
/// println!(" Active: {}", go.is_active());
|
||||
/// println!(" Components: {}", go.components().len());
|
||||
/// }
|
||||
/// }
|
||||
/// # Ok::<(), cursebreaker_parser::Error>(())
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct GameObject<'a> {
|
||||
document: &'a UnityDocument,
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GameObject {
|
||||
name: Option<String>,
|
||||
is_active: bool,
|
||||
layer: Option<i64>,
|
||||
tag: Option<i64>,
|
||||
}
|
||||
|
||||
impl<'a> GameObject<'a> {
|
||||
/// Create a GameObject wrapper from a UnityDocument
|
||||
impl GameObject {
|
||||
/// Parse a GameObject from a UnityDocument
|
||||
///
|
||||
/// Returns None if the document is not a GameObject.
|
||||
pub fn new(document: &'a UnityDocument) -> Option<Self> {
|
||||
if document.is_game_object() {
|
||||
Some(Self { document })
|
||||
} else {
|
||||
None
|
||||
pub fn parse(document: &UnityDocument) -> Option<Self> {
|
||||
if !document.is_game_object() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Get the GameObject properties object
|
||||
let props = document
|
||||
.get("GameObject")
|
||||
.and_then(|obj| obj.as_object());
|
||||
|
||||
let name = props
|
||||
.and_then(|p| p.get("m_Name"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let is_active = props
|
||||
.and_then(|p| p.get("m_IsActive"))
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(true);
|
||||
|
||||
let layer = props
|
||||
.and_then(|p| p.get("m_Layer"))
|
||||
.and_then(|v| v.as_i64());
|
||||
|
||||
let tag = props
|
||||
.and_then(|p| p.get("m_TagString"))
|
||||
.and_then(|v| v.as_i64());
|
||||
|
||||
Some(Self {
|
||||
name,
|
||||
is_active,
|
||||
layer,
|
||||
tag,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the GameObject's name
|
||||
pub fn name(&self) -> Option<&str> {
|
||||
self.document
|
||||
.get("GameObject")
|
||||
.and_then(|obj| obj.as_object())
|
||||
.and_then(|props| props.get("m_Name"))
|
||||
.and_then(|v| v.as_str())
|
||||
self.name.as_deref()
|
||||
}
|
||||
|
||||
/// Check if the GameObject is active
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.document
|
||||
.get("GameObject")
|
||||
.and_then(|obj| obj.as_object())
|
||||
.and_then(|props| props.get("m_IsActive"))
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(true) // Default to true if not specified
|
||||
self.is_active
|
||||
}
|
||||
|
||||
/// Get the GameObject's layer
|
||||
pub fn layer(&self) -> Option<i64> {
|
||||
self.document
|
||||
.get("GameObject")
|
||||
.and_then(|obj| obj.as_object())
|
||||
.and_then(|props| props.get("m_Layer"))
|
||||
.and_then(|v| v.as_i64())
|
||||
self.layer
|
||||
}
|
||||
|
||||
/// Get the GameObject's tag as a tag ID
|
||||
pub fn tag(&self) -> Option<i64> {
|
||||
self.document
|
||||
.get("GameObject")
|
||||
.and_then(|obj| obj.as_object())
|
||||
.and_then(|props| props.get("m_TagString"))
|
||||
.and_then(|v| v.as_i64())
|
||||
}
|
||||
|
||||
/// Get the list of component references attached to this GameObject
|
||||
pub fn components(&self) -> Vec<FileRef> {
|
||||
self.document
|
||||
.get("GameObject")
|
||||
.and_then(|obj| obj.as_object())
|
||||
.and_then(|props| props.get("m_Component"))
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|item| {
|
||||
item.as_object().and_then(|obj| {
|
||||
obj.get("component")
|
||||
.and_then(|v| v.as_file_ref())
|
||||
.copied()
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get the file ID of this GameObject
|
||||
pub fn file_id(&self) -> FileID {
|
||||
self.document.file_id
|
||||
}
|
||||
|
||||
/// Get the underlying UnityDocument
|
||||
pub fn document(&self) -> &'a UnityDocument {
|
||||
self.document
|
||||
self.tag
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::model::UnityDocument;
|
||||
use crate::property::PropertyValue;
|
||||
use crate::types::FileID;
|
||||
use indexmap::IndexMap;
|
||||
|
||||
fn create_test_game_object() -> UnityDocument {
|
||||
@@ -121,7 +104,6 @@ mod tests {
|
||||
go_props.insert("m_Name".to_string(), PropertyValue::String("TestObject".to_string()));
|
||||
go_props.insert("m_IsActive".to_string(), PropertyValue::Boolean(true));
|
||||
go_props.insert("m_Layer".to_string(), PropertyValue::Integer(0));
|
||||
go_props.insert("m_Component".to_string(), PropertyValue::Array(vec![]));
|
||||
|
||||
properties.insert("GameObject".to_string(), PropertyValue::Object(go_props));
|
||||
|
||||
@@ -136,45 +118,38 @@ mod tests {
|
||||
#[test]
|
||||
fn test_game_object_creation() {
|
||||
let doc = create_test_game_object();
|
||||
let go = GameObject::new(&doc);
|
||||
let go = GameObject::parse(&doc);
|
||||
assert!(go.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_game_object_name() {
|
||||
let doc = create_test_game_object();
|
||||
let go = GameObject::new(&doc).unwrap();
|
||||
let go = GameObject::parse(&doc).unwrap();
|
||||
assert_eq!(go.name(), Some("TestObject"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_game_object_is_active() {
|
||||
let doc = create_test_game_object();
|
||||
let go = GameObject::new(&doc).unwrap();
|
||||
let go = GameObject::parse(&doc).unwrap();
|
||||
assert!(go.is_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_game_object_layer() {
|
||||
let doc = create_test_game_object();
|
||||
let go = GameObject::new(&doc).unwrap();
|
||||
let go = GameObject::parse(&doc).unwrap();
|
||||
assert_eq!(go.layer(), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_game_object_file_id() {
|
||||
let doc = create_test_game_object();
|
||||
let go = GameObject::new(&doc).unwrap();
|
||||
assert_eq!(go.file_id().as_i64(), 12345);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_game_object() {
|
||||
let mut doc = create_test_game_object();
|
||||
doc.type_id = 4; // Transform type ID
|
||||
doc.class_name = "Transform".to_string();
|
||||
|
||||
let go = GameObject::new(&doc);
|
||||
let go = GameObject::parse(&doc);
|
||||
assert!(go.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,15 @@
|
||||
mod component;
|
||||
mod game_object;
|
||||
mod ids;
|
||||
mod reference;
|
||||
mod transform;
|
||||
mod type_registry;
|
||||
mod values;
|
||||
|
||||
pub use component::{Component, GenericComponent};
|
||||
pub use game_object::GameObject;
|
||||
pub use ids::{FileID, LocalID};
|
||||
pub use reference::UnityReference;
|
||||
pub use transform::{RectTransform, Transform};
|
||||
pub use type_registry::{get_class_name, get_type_id};
|
||||
pub use values::{Color, ExternalRef, FileRef, Quaternion, Vector2, Vector3};
|
||||
|
||||
291
src/types/reference.rs
Normal file
291
src/types/reference.rs
Normal file
@@ -0,0 +1,291 @@
|
||||
//! Unified reference type for Unity object references
|
||||
//!
|
||||
//! This module provides the UnityReference enum which abstracts over
|
||||
//! local references (FileRef) and external references (ExternalRef).
|
||||
|
||||
use crate::types::{ExternalRef, FileID, FileRef};
|
||||
|
||||
/// A unified reference type that can be local, external, or null
|
||||
///
|
||||
/// Unity uses different reference patterns:
|
||||
/// - Local: `{fileID: N}` - references an object in the same file
|
||||
/// - External: `{fileID: M, guid: XXXX, type: N}` - references an external asset
|
||||
/// - Null: `{fileID: 0}` - null/empty reference
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum UnityReference {
|
||||
/// Reference to an object in the same file by FileID
|
||||
Local(FileID),
|
||||
|
||||
/// Reference to an external asset by GUID
|
||||
External {
|
||||
/// The GUID of the external asset
|
||||
guid: String,
|
||||
/// The Unity type ID
|
||||
type_id: i32,
|
||||
/// Optional resolved FileID within the external file
|
||||
file_id: Option<FileID>,
|
||||
},
|
||||
|
||||
/// Null reference (fileID: 0)
|
||||
Null,
|
||||
}
|
||||
|
||||
impl UnityReference {
|
||||
/// Create a UnityReference from a FileRef
|
||||
///
|
||||
/// FileID of 0 is treated as a null reference.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use cursebreaker_parser::types::{UnityReference, FileRef, FileID};
|
||||
///
|
||||
/// // Local reference
|
||||
/// let file_ref = FileRef::new(FileID::from_i64(12345));
|
||||
/// let reference = UnityReference::from_file_ref(&file_ref);
|
||||
/// assert!(reference.is_local());
|
||||
///
|
||||
/// // Null reference
|
||||
/// let null_ref = FileRef::new(FileID::from_i64(0));
|
||||
/// let reference = UnityReference::from_file_ref(&null_ref);
|
||||
/// assert!(reference.is_null());
|
||||
/// ```
|
||||
pub fn from_file_ref(file_ref: &FileRef) -> Self {
|
||||
if file_ref.file_id.as_i64() == 0 {
|
||||
UnityReference::Null
|
||||
} else {
|
||||
UnityReference::Local(file_ref.file_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a UnityReference from an ExternalRef
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use cursebreaker_parser::types::{UnityReference, ExternalRef};
|
||||
///
|
||||
/// let ext_ref = ExternalRef::new("abc123".to_string(), 2);
|
||||
/// let reference = UnityReference::from_external_ref(&ext_ref);
|
||||
/// assert!(reference.is_external());
|
||||
/// ```
|
||||
pub fn from_external_ref(ext_ref: &ExternalRef) -> Self {
|
||||
UnityReference::External {
|
||||
guid: ext_ref.guid.clone(),
|
||||
type_id: ext_ref.type_id,
|
||||
file_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this is a null reference
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use cursebreaker_parser::types::UnityReference;
|
||||
///
|
||||
/// let reference = UnityReference::Null;
|
||||
/// assert!(reference.is_null());
|
||||
/// ```
|
||||
pub fn is_null(&self) -> bool {
|
||||
matches!(self, UnityReference::Null)
|
||||
}
|
||||
|
||||
/// Check if this is a local reference
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use cursebreaker_parser::types::{UnityReference, FileID};
|
||||
///
|
||||
/// let reference = UnityReference::Local(FileID::from_i64(12345));
|
||||
/// assert!(reference.is_local());
|
||||
/// ```
|
||||
pub fn is_local(&self) -> bool {
|
||||
matches!(self, UnityReference::Local(_))
|
||||
}
|
||||
|
||||
/// Check if this is an external reference
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use cursebreaker_parser::types::UnityReference;
|
||||
///
|
||||
/// let reference = UnityReference::External {
|
||||
/// guid: "abc123".to_string(),
|
||||
/// type_id: 2,
|
||||
/// file_id: None,
|
||||
/// };
|
||||
/// assert!(reference.is_external());
|
||||
/// ```
|
||||
pub fn is_external(&self) -> bool {
|
||||
matches!(self, UnityReference::External { .. })
|
||||
}
|
||||
|
||||
/// Get the FileID for local references
|
||||
///
|
||||
/// Returns None for external or null references.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use cursebreaker_parser::types::{UnityReference, FileID};
|
||||
///
|
||||
/// let reference = UnityReference::Local(FileID::from_i64(12345));
|
||||
/// assert_eq!(reference.as_file_id(), Some(FileID::from_i64(12345)));
|
||||
///
|
||||
/// let null_ref = UnityReference::Null;
|
||||
/// assert_eq!(null_ref.as_file_id(), None);
|
||||
/// ```
|
||||
pub fn as_file_id(&self) -> Option<FileID> {
|
||||
match self {
|
||||
UnityReference::Local(file_id) => Some(*file_id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the GUID for external references
|
||||
///
|
||||
/// Returns None for local or null references.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use cursebreaker_parser::types::UnityReference;
|
||||
///
|
||||
/// let reference = UnityReference::External {
|
||||
/// guid: "abc123".to_string(),
|
||||
/// type_id: 2,
|
||||
/// file_id: None,
|
||||
/// };
|
||||
/// assert_eq!(reference.as_guid(), Some("abc123"));
|
||||
/// ```
|
||||
pub fn as_guid(&self) -> Option<&str> {
|
||||
match self {
|
||||
UnityReference::External { guid, .. } => Some(guid),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_from_file_ref_local() {
|
||||
let file_ref = FileRef::new(FileID::from_i64(12345));
|
||||
let reference = UnityReference::from_file_ref(&file_ref);
|
||||
assert!(reference.is_local());
|
||||
assert_eq!(reference.as_file_id(), Some(FileID::from_i64(12345)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_file_ref_null() {
|
||||
let file_ref = FileRef::new(FileID::from_i64(0));
|
||||
let reference = UnityReference::from_file_ref(&file_ref);
|
||||
assert!(reference.is_null());
|
||||
assert!(!reference.is_local());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_external_ref() {
|
||||
let ext_ref = ExternalRef::new("abc123".to_string(), 2);
|
||||
let reference = UnityReference::from_external_ref(&ext_ref);
|
||||
assert!(reference.is_external());
|
||||
assert_eq!(reference.as_guid(), Some("abc123"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_null_reference() {
|
||||
let reference = UnityReference::Null;
|
||||
assert!(reference.is_null());
|
||||
assert!(!reference.is_local());
|
||||
assert!(!reference.is_external());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_local_reference() {
|
||||
let reference = UnityReference::Local(FileID::from_i64(999));
|
||||
assert!(reference.is_local());
|
||||
assert!(!reference.is_null());
|
||||
assert!(!reference.is_external());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_external_reference() {
|
||||
let reference = UnityReference::External {
|
||||
guid: "test-guid".to_string(),
|
||||
type_id: 3,
|
||||
file_id: None,
|
||||
};
|
||||
assert!(reference.is_external());
|
||||
assert!(!reference.is_null());
|
||||
assert!(!reference.is_local());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_equality() {
|
||||
let ref1 = UnityReference::Local(FileID::from_i64(123));
|
||||
let ref2 = UnityReference::Local(FileID::from_i64(123));
|
||||
let ref3 = UnityReference::Local(FileID::from_i64(456));
|
||||
|
||||
assert_eq!(ref1, ref2);
|
||||
assert_ne!(ref1, ref3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_external_equality() {
|
||||
let ref1 = UnityReference::External {
|
||||
guid: "abc".to_string(),
|
||||
type_id: 2,
|
||||
file_id: None,
|
||||
};
|
||||
let ref2 = UnityReference::External {
|
||||
guid: "abc".to_string(),
|
||||
type_id: 2,
|
||||
file_id: None,
|
||||
};
|
||||
let ref3 = UnityReference::External {
|
||||
guid: "xyz".to_string(),
|
||||
type_id: 2,
|
||||
file_id: None,
|
||||
};
|
||||
|
||||
assert_eq!(ref1, ref2);
|
||||
assert_ne!(ref1, ref3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_as_file_id() {
|
||||
let local = UnityReference::Local(FileID::from_i64(12345));
|
||||
assert_eq!(local.as_file_id(), Some(FileID::from_i64(12345)));
|
||||
|
||||
let null = UnityReference::Null;
|
||||
assert_eq!(null.as_file_id(), None);
|
||||
|
||||
let external = UnityReference::External {
|
||||
guid: "test".to_string(),
|
||||
type_id: 1,
|
||||
file_id: None,
|
||||
};
|
||||
assert_eq!(external.as_file_id(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_as_guid() {
|
||||
let external = UnityReference::External {
|
||||
guid: "test-guid".to_string(),
|
||||
type_id: 1,
|
||||
file_id: None,
|
||||
};
|
||||
assert_eq!(external.as_guid(), Some("test-guid"));
|
||||
|
||||
let local = UnityReference::Local(FileID::from_i64(123));
|
||||
assert_eq!(local.as_guid(), None);
|
||||
|
||||
let null = UnityReference::Null;
|
||||
assert_eq!(null.as_guid(), None);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
//! Transform and RectTransform component wrappers
|
||||
|
||||
use crate::model::UnityDocument;
|
||||
use crate::types::{Component, FileRef, Quaternion, Vector2, Vector3};
|
||||
use crate::types::{Quaternion, Vector2, Vector3};
|
||||
use sparsey::Entity;
|
||||
|
||||
/// A wrapper around a UnityDocument that represents a Transform component
|
||||
///
|
||||
@@ -14,7 +15,7 @@ use crate::types::{Component, FileRef, Quaternion, Vector2, Vector3};
|
||||
///
|
||||
/// let file = UnityFile::from_path("Scene.unity")?;
|
||||
/// for doc in &file.documents {
|
||||
/// if let Some(transform) = Transform::new(doc) {
|
||||
/// if let Some(transform) = Transform::parse(doc) {
|
||||
/// if let Some(pos) = transform.local_position() {
|
||||
/// println!("Position: ({}, {}, {})", pos.x, pos.y, pos.z);
|
||||
/// }
|
||||
@@ -22,88 +23,94 @@ use crate::types::{Component, FileRef, Quaternion, Vector2, Vector3};
|
||||
/// }
|
||||
/// # Ok::<(), cursebreaker_parser::Error>(())
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct Transform<'a> {
|
||||
document: &'a UnityDocument,
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Transform {
|
||||
local_position: Option<Vector3>,
|
||||
local_rotation: Option<Quaternion>,
|
||||
local_scale: Option<Vector3>,
|
||||
parent: Option<Entity>,
|
||||
children: Vec<Entity>,
|
||||
}
|
||||
|
||||
impl<'a> Transform<'a> {
|
||||
/// Create a Transform wrapper from a UnityDocument
|
||||
impl Transform {
|
||||
/// Parse a Transform from a UnityDocument
|
||||
///
|
||||
/// Returns None if the document is not a Transform or RectTransform.
|
||||
pub fn new(document: &'a UnityDocument) -> Option<Self> {
|
||||
if document.class_name == "Transform" || document.class_name == "RectTransform" {
|
||||
Some(Self { document })
|
||||
} else {
|
||||
None
|
||||
pub fn parse(document: &UnityDocument) -> Option<Self> {
|
||||
if document.class_name != "Transform" && document.class_name != "RectTransform" {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Get the transform properties object
|
||||
let props = document
|
||||
.get(&document.class_name)
|
||||
.and_then(|v| v.as_object());
|
||||
|
||||
let local_position = props
|
||||
.and_then(|p| p.get("m_LocalPosition"))
|
||||
.and_then(|v| v.as_vector3())
|
||||
.copied();
|
||||
|
||||
let local_rotation = props
|
||||
.and_then(|p| p.get("m_LocalRotation"))
|
||||
.and_then(|v| v.as_quaternion())
|
||||
.copied();
|
||||
|
||||
let local_scale = props
|
||||
.and_then(|p| p.get("m_LocalScale"))
|
||||
.and_then(|v| v.as_vector3())
|
||||
.copied();
|
||||
|
||||
// Note: parent and children entities will be set later when building the ECS world
|
||||
// The FileRef data is still in the UnityDocument but needs to be converted to entities separately
|
||||
|
||||
Some(Self {
|
||||
local_position,
|
||||
local_rotation,
|
||||
local_scale,
|
||||
parent: None,
|
||||
children: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the local position of this transform
|
||||
pub fn local_position(&self) -> Option<&Vector3> {
|
||||
self.get_transform_props()
|
||||
.and_then(|props| props.get("m_LocalPosition"))
|
||||
.and_then(|v| v.as_vector3())
|
||||
self.local_position.as_ref()
|
||||
}
|
||||
|
||||
/// Get the local rotation of this transform
|
||||
pub fn local_rotation(&self) -> Option<&Quaternion> {
|
||||
self.get_transform_props()
|
||||
.and_then(|props| props.get("m_LocalRotation"))
|
||||
.and_then(|v| v.as_quaternion())
|
||||
self.local_rotation.as_ref()
|
||||
}
|
||||
|
||||
/// Get the local scale of this transform
|
||||
pub fn local_scale(&self) -> Option<&Vector3> {
|
||||
self.get_transform_props()
|
||||
.and_then(|props| props.get("m_LocalScale"))
|
||||
.and_then(|v| v.as_vector3())
|
||||
self.local_scale.as_ref()
|
||||
}
|
||||
|
||||
/// Get the parent transform reference
|
||||
pub fn parent(&self) -> Option<FileRef> {
|
||||
self.get_transform_props()
|
||||
.and_then(|props| props.get("m_Father"))
|
||||
.and_then(|v| v.as_file_ref())
|
||||
.copied()
|
||||
pub fn parent(&self) -> Option<Entity> {
|
||||
self.parent
|
||||
}
|
||||
|
||||
/// Set the parent transform entity
|
||||
pub fn set_parent(&mut self, parent: Option<Entity>) {
|
||||
self.parent = parent;
|
||||
}
|
||||
|
||||
/// Get the list of child transform references
|
||||
pub fn children(&self) -> Vec<FileRef> {
|
||||
self.get_transform_props()
|
||||
.and_then(|props| props.get("m_Children"))
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|item| item.as_file_ref())
|
||||
.copied()
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
pub fn children(&self) -> &[Entity] {
|
||||
&self.children
|
||||
}
|
||||
|
||||
/// Helper to get the transform properties object
|
||||
fn get_transform_props(&self) -> Option<&indexmap::IndexMap<String, crate::property::PropertyValue>> {
|
||||
self.document
|
||||
.get(&self.document.class_name)
|
||||
.and_then(|v| v.as_object())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Component for Transform<'a> {
|
||||
fn game_object(&self) -> Option<FileRef> {
|
||||
self.get_transform_props()
|
||||
.and_then(|props| props.get("m_GameObject"))
|
||||
.and_then(|v| v.as_file_ref())
|
||||
.copied()
|
||||
/// Set the child transform entities
|
||||
pub fn set_children(&mut self, children: Vec<Entity>) {
|
||||
self.children = children;
|
||||
}
|
||||
|
||||
fn is_enabled(&self) -> bool {
|
||||
true // Transforms are always enabled
|
||||
}
|
||||
|
||||
fn document(&self) -> &UnityDocument {
|
||||
self.document
|
||||
/// Add a child transform entity
|
||||
pub fn add_child(&mut self, child: Entity) {
|
||||
self.children.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +125,7 @@ impl<'a> Component for Transform<'a> {
|
||||
///
|
||||
/// let file = UnityFile::from_path("Canvas.prefab")?;
|
||||
/// for doc in &file.documents {
|
||||
/// if let Some(rect_transform) = RectTransform::new(doc) {
|
||||
/// if let Some(rect_transform) = RectTransform::parse(doc) {
|
||||
/// if let Some(anchor_min) = rect_transform.anchor_min() {
|
||||
/// println!("Anchor Min: ({}, {})", anchor_min.x, anchor_min.y);
|
||||
/// }
|
||||
@@ -126,58 +133,91 @@ impl<'a> Component for Transform<'a> {
|
||||
/// }
|
||||
/// # Ok::<(), cursebreaker_parser::Error>(())
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct RectTransform<'a> {
|
||||
transform: Transform<'a>,
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RectTransform {
|
||||
transform: Transform,
|
||||
anchor_min: Option<Vector2>,
|
||||
anchor_max: Option<Vector2>,
|
||||
anchored_position: Option<Vector2>,
|
||||
size_delta: Option<Vector2>,
|
||||
pivot: Option<Vector2>,
|
||||
}
|
||||
|
||||
impl<'a> RectTransform<'a> {
|
||||
/// Create a RectTransform wrapper from a UnityDocument
|
||||
impl RectTransform {
|
||||
/// Parse a RectTransform from a UnityDocument
|
||||
///
|
||||
/// Returns None if the document is not a RectTransform.
|
||||
pub fn new(document: &'a UnityDocument) -> Option<Self> {
|
||||
if document.class_name == "RectTransform" {
|
||||
Some(Self {
|
||||
transform: Transform { document },
|
||||
})
|
||||
} else {
|
||||
None
|
||||
pub fn parse(document: &UnityDocument) -> Option<Self> {
|
||||
if document.class_name != "RectTransform" {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Parse the base Transform
|
||||
let transform = Transform::parse(document)?;
|
||||
|
||||
// Get the RectTransform properties object
|
||||
let props = document
|
||||
.get("RectTransform")
|
||||
.and_then(|v| v.as_object());
|
||||
|
||||
let anchor_min = props
|
||||
.and_then(|p| p.get("m_AnchorMin"))
|
||||
.and_then(|v| v.as_vector2())
|
||||
.copied();
|
||||
|
||||
let anchor_max = props
|
||||
.and_then(|p| p.get("m_AnchorMax"))
|
||||
.and_then(|v| v.as_vector2())
|
||||
.copied();
|
||||
|
||||
let anchored_position = props
|
||||
.and_then(|p| p.get("m_AnchoredPosition"))
|
||||
.and_then(|v| v.as_vector2())
|
||||
.copied();
|
||||
|
||||
let size_delta = props
|
||||
.and_then(|p| p.get("m_SizeDelta"))
|
||||
.and_then(|v| v.as_vector2())
|
||||
.copied();
|
||||
|
||||
let pivot = props
|
||||
.and_then(|p| p.get("m_Pivot"))
|
||||
.and_then(|v| v.as_vector2())
|
||||
.copied();
|
||||
|
||||
Some(Self {
|
||||
transform,
|
||||
anchor_min,
|
||||
anchor_max,
|
||||
anchored_position,
|
||||
size_delta,
|
||||
pivot,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the anchor min (bottom-left anchor)
|
||||
pub fn anchor_min(&self) -> Option<&Vector2> {
|
||||
self.get_rect_props()
|
||||
.and_then(|props| props.get("m_AnchorMin"))
|
||||
.and_then(|v| v.as_vector2())
|
||||
self.anchor_min.as_ref()
|
||||
}
|
||||
|
||||
/// Get the anchor max (top-right anchor)
|
||||
pub fn anchor_max(&self) -> Option<&Vector2> {
|
||||
self.get_rect_props()
|
||||
.and_then(|props| props.get("m_AnchorMax"))
|
||||
.and_then(|v| v.as_vector2())
|
||||
self.anchor_max.as_ref()
|
||||
}
|
||||
|
||||
/// Get the anchored position
|
||||
pub fn anchored_position(&self) -> Option<&Vector2> {
|
||||
self.get_rect_props()
|
||||
.and_then(|props| props.get("m_AnchoredPosition"))
|
||||
.and_then(|v| v.as_vector2())
|
||||
self.anchored_position.as_ref()
|
||||
}
|
||||
|
||||
/// Get the size delta
|
||||
pub fn size_delta(&self) -> Option<&Vector2> {
|
||||
self.get_rect_props()
|
||||
.and_then(|props| props.get("m_SizeDelta"))
|
||||
.and_then(|v| v.as_vector2())
|
||||
self.size_delta.as_ref()
|
||||
}
|
||||
|
||||
/// Get the pivot point
|
||||
pub fn pivot(&self) -> Option<&Vector2> {
|
||||
self.get_rect_props()
|
||||
.and_then(|props| props.get("m_Pivot"))
|
||||
.and_then(|v| v.as_vector2())
|
||||
self.pivot.as_ref()
|
||||
}
|
||||
|
||||
/// Get the local position (from Transform)
|
||||
@@ -196,38 +236,35 @@ impl<'a> RectTransform<'a> {
|
||||
}
|
||||
|
||||
/// Get the parent transform reference (from Transform)
|
||||
pub fn parent(&self) -> Option<FileRef> {
|
||||
pub fn parent(&self) -> Option<Entity> {
|
||||
self.transform.parent()
|
||||
}
|
||||
|
||||
/// Set the parent transform entity (from Transform)
|
||||
pub fn set_parent(&mut self, parent: Option<Entity>) {
|
||||
self.transform.set_parent(parent);
|
||||
}
|
||||
|
||||
/// Get the list of child transform references (from Transform)
|
||||
pub fn children(&self) -> Vec<FileRef> {
|
||||
pub fn children(&self) -> &[Entity] {
|
||||
self.transform.children()
|
||||
}
|
||||
|
||||
/// Helper to get the RectTransform properties object
|
||||
fn get_rect_props(&self) -> Option<&indexmap::IndexMap<String, crate::property::PropertyValue>> {
|
||||
self.transform.get_transform_props()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Component for RectTransform<'a> {
|
||||
fn game_object(&self) -> Option<FileRef> {
|
||||
self.transform.game_object()
|
||||
/// Set the child transform entities (from Transform)
|
||||
pub fn set_children(&mut self, children: Vec<Entity>) {
|
||||
self.transform.set_children(children);
|
||||
}
|
||||
|
||||
fn is_enabled(&self) -> bool {
|
||||
self.transform.is_enabled()
|
||||
}
|
||||
|
||||
fn document(&self) -> &UnityDocument {
|
||||
self.transform.document()
|
||||
/// Add a child transform entity (from Transform)
|
||||
pub fn add_child(&mut self, child: Entity) {
|
||||
self.transform.add_child(child);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::model::UnityDocument;
|
||||
use crate::property::PropertyValue;
|
||||
use crate::types::FileID;
|
||||
use indexmap::IndexMap;
|
||||
@@ -248,11 +285,6 @@ mod tests {
|
||||
"m_LocalScale".to_string(),
|
||||
PropertyValue::Vector3(Vector3::ONE),
|
||||
);
|
||||
transform_props.insert(
|
||||
"m_GameObject".to_string(),
|
||||
PropertyValue::FileRef(crate::types::FileRef::new(FileID::from_i64(67890))),
|
||||
);
|
||||
transform_props.insert("m_Children".to_string(), PropertyValue::Array(vec![]));
|
||||
|
||||
properties.insert("Transform".to_string(), PropertyValue::Object(transform_props));
|
||||
|
||||
@@ -314,14 +346,14 @@ mod tests {
|
||||
#[test]
|
||||
fn test_transform_creation() {
|
||||
let doc = create_test_transform();
|
||||
let transform = Transform::new(&doc);
|
||||
let transform = Transform::parse(&doc);
|
||||
assert!(transform.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transform_local_position() {
|
||||
let doc = create_test_transform();
|
||||
let transform = Transform::new(&doc).unwrap();
|
||||
let transform = Transform::parse(&doc).unwrap();
|
||||
let pos = transform.local_position().unwrap();
|
||||
assert_eq!(pos.x, 1.0);
|
||||
assert_eq!(pos.y, 2.0);
|
||||
@@ -331,7 +363,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_transform_local_rotation() {
|
||||
let doc = create_test_transform();
|
||||
let transform = Transform::new(&doc).unwrap();
|
||||
let transform = Transform::parse(&doc).unwrap();
|
||||
let rot = transform.local_rotation().unwrap();
|
||||
assert_eq!(rot.w, 1.0);
|
||||
}
|
||||
@@ -339,7 +371,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_transform_local_scale() {
|
||||
let doc = create_test_transform();
|
||||
let transform = Transform::new(&doc).unwrap();
|
||||
let transform = Transform::parse(&doc).unwrap();
|
||||
let scale = transform.local_scale().unwrap();
|
||||
assert_eq!(scale, &Vector3::ONE);
|
||||
}
|
||||
@@ -347,14 +379,14 @@ mod tests {
|
||||
#[test]
|
||||
fn test_rect_transform_creation() {
|
||||
let doc = create_test_rect_transform();
|
||||
let rect_transform = RectTransform::new(&doc);
|
||||
let rect_transform = RectTransform::parse(&doc);
|
||||
assert!(rect_transform.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rect_transform_anchor_min() {
|
||||
let doc = create_test_rect_transform();
|
||||
let rect_transform = RectTransform::new(&doc).unwrap();
|
||||
let rect_transform = RectTransform::parse(&doc).unwrap();
|
||||
let anchor_min = rect_transform.anchor_min().unwrap();
|
||||
assert_eq!(anchor_min, &Vector2::ZERO);
|
||||
}
|
||||
@@ -362,7 +394,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_rect_transform_anchor_max() {
|
||||
let doc = create_test_rect_transform();
|
||||
let rect_transform = RectTransform::new(&doc).unwrap();
|
||||
let rect_transform = RectTransform::parse(&doc).unwrap();
|
||||
let anchor_max = rect_transform.anchor_max().unwrap();
|
||||
assert_eq!(anchor_max, &Vector2::ONE);
|
||||
}
|
||||
@@ -370,7 +402,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_rect_transform_size_delta() {
|
||||
let doc = create_test_rect_transform();
|
||||
let rect_transform = RectTransform::new(&doc).unwrap();
|
||||
let rect_transform = RectTransform::parse(&doc).unwrap();
|
||||
let size_delta = rect_transform.size_delta().unwrap();
|
||||
assert_eq!(size_delta.x, 100.0);
|
||||
assert_eq!(size_delta.y, 50.0);
|
||||
@@ -379,7 +411,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_rect_transform_pivot() {
|
||||
let doc = create_test_rect_transform();
|
||||
let rect_transform = RectTransform::new(&doc).unwrap();
|
||||
let rect_transform = RectTransform::parse(&doc).unwrap();
|
||||
let pivot = rect_transform.pivot().unwrap();
|
||||
assert_eq!(pivot.x, 0.5);
|
||||
assert_eq!(pivot.y, 0.5);
|
||||
@@ -388,7 +420,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_rect_transform_inherits_from_transform() {
|
||||
let doc = create_test_rect_transform();
|
||||
let rect_transform = RectTransform::new(&doc).unwrap();
|
||||
let rect_transform = RectTransform::parse(&doc).unwrap();
|
||||
|
||||
// Test inherited methods
|
||||
assert!(rect_transform.local_position().is_some());
|
||||
|
||||
354
src/types/type_registry.rs
Normal file
354
src/types/type_registry.rs
Normal file
@@ -0,0 +1,354 @@
|
||||
//! Unity type ID to class name mapping
|
||||
//!
|
||||
//! This module provides a centralized registry for mapping Unity type IDs
|
||||
//! to their corresponding class names and vice versa.
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Unity type ID to class name registry
|
||||
pub struct UnityTypeRegistry {
|
||||
type_to_name: HashMap<u32, &'static str>,
|
||||
name_to_type: HashMap<&'static str, u32>,
|
||||
}
|
||||
|
||||
impl UnityTypeRegistry {
|
||||
/// Create a new empty registry
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
type_to_name: HashMap::new(),
|
||||
name_to_type: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a Unity type ID with its class name
|
||||
fn register(&mut self, type_id: u32, class_name: &'static str) {
|
||||
self.type_to_name.insert(type_id, class_name);
|
||||
self.name_to_type.insert(class_name, type_id);
|
||||
}
|
||||
|
||||
/// Get the class name for a Unity type ID
|
||||
pub fn get_class_name(&self, type_id: u32) -> Option<&'static str> {
|
||||
self.type_to_name.get(&type_id).copied()
|
||||
}
|
||||
|
||||
/// Get the type ID for a Unity class name
|
||||
pub fn get_type_id(&self, class_name: &str) -> Option<u32> {
|
||||
self.name_to_type.get(class_name).copied()
|
||||
}
|
||||
}
|
||||
|
||||
/// Global Unity type registry (lazily initialized)
|
||||
pub static UNITY_TYPE_REGISTRY: Lazy<UnityTypeRegistry> = Lazy::new(|| {
|
||||
let mut registry = UnityTypeRegistry::new();
|
||||
|
||||
// Core Unity types
|
||||
registry.register(1, "GameObject");
|
||||
registry.register(2, "Component");
|
||||
registry.register(3, "LevelGameManager");
|
||||
registry.register(4, "Transform");
|
||||
registry.register(5, "TimeManager");
|
||||
registry.register(8, "Behaviour");
|
||||
registry.register(9, "GameManager");
|
||||
registry.register(11, "AudioManager");
|
||||
registry.register(13, "InputManager");
|
||||
registry.register(18, "EditorExtension");
|
||||
registry.register(19, "Physics2DSettings");
|
||||
registry.register(20, "Camera");
|
||||
registry.register(21, "Material");
|
||||
registry.register(23, "MeshRenderer");
|
||||
registry.register(25, "Renderer");
|
||||
registry.register(27, "Texture");
|
||||
registry.register(28, "Texture2D");
|
||||
registry.register(29, "OcclusionCullingSettings");
|
||||
registry.register(33, "MeshFilter");
|
||||
registry.register(41, "OcclusionPortal");
|
||||
registry.register(43, "Mesh");
|
||||
registry.register(45, "Skybox");
|
||||
registry.register(47, "QualitySettings");
|
||||
registry.register(48, "Shader");
|
||||
registry.register(49, "TextAsset");
|
||||
registry.register(50, "Rigidbody2D");
|
||||
registry.register(53, "Collider2D");
|
||||
registry.register(54, "Rigidbody");
|
||||
registry.register(55, "PhysicMaterial");
|
||||
registry.register(56, "Collider");
|
||||
registry.register(57, "Joint");
|
||||
registry.register(58, "CircleCollider2D");
|
||||
registry.register(59, "HingeJoint");
|
||||
registry.register(60, "PolygonCollider2D");
|
||||
registry.register(61, "BoxCollider2D");
|
||||
registry.register(62, "PhysicsMaterial2D");
|
||||
registry.register(64, "MeshCollider");
|
||||
registry.register(65, "BoxCollider");
|
||||
registry.register(68, "EdgeCollider2D");
|
||||
registry.register(70, "CapsuleCollider2D");
|
||||
registry.register(72, "CompositeCollider2D");
|
||||
registry.register(74, "AnimationClip");
|
||||
registry.register(75, "ConstantForce");
|
||||
registry.register(81, "AudioListener");
|
||||
registry.register(82, "AudioSource");
|
||||
registry.register(83, "AudioClip");
|
||||
registry.register(84, "RenderTexture");
|
||||
registry.register(87, "MeshParticleEmitter");
|
||||
registry.register(88, "ParticleEmitter");
|
||||
registry.register(89, "Cubemap");
|
||||
registry.register(90, "Avatar");
|
||||
registry.register(91, "AnimatorController");
|
||||
registry.register(92, "GUILayer");
|
||||
registry.register(93, "RuntimeAnimatorController");
|
||||
registry.register(94, "ScriptMapper");
|
||||
registry.register(95, "Animator");
|
||||
registry.register(96, "TrailRenderer");
|
||||
registry.register(98, "TextMesh");
|
||||
registry.register(102, "TextureImporter");
|
||||
registry.register(104, "RenderSettings");
|
||||
registry.register(108, "Light");
|
||||
registry.register(109, "CGProgram");
|
||||
registry.register(110, "BaseAnimationTrack");
|
||||
registry.register(111, "Animation");
|
||||
registry.register(114, "MonoBehaviour");
|
||||
registry.register(115, "MonoScript");
|
||||
registry.register(116, "MonoManager");
|
||||
registry.register(117, "Texture3D");
|
||||
registry.register(118, "NewAnimationTrack");
|
||||
registry.register(119, "Projector");
|
||||
registry.register(120, "LineRenderer");
|
||||
registry.register(121, "Flare");
|
||||
registry.register(122, "Halo");
|
||||
registry.register(123, "LensFlare");
|
||||
registry.register(124, "FlareLayer");
|
||||
registry.register(125, "HaloLayer");
|
||||
registry.register(126, "NavMeshAreas");
|
||||
registry.register(127, "HaloManager");
|
||||
registry.register(128, "Font");
|
||||
registry.register(129, "PlayerSettings");
|
||||
registry.register(130, "NamedObject");
|
||||
registry.register(131, "GUITexture");
|
||||
registry.register(132, "GUIText");
|
||||
registry.register(133, "GUIElement");
|
||||
registry.register(134, "PhysicMaterial");
|
||||
registry.register(135, "SphereCollider");
|
||||
registry.register(136, "CapsuleCollider");
|
||||
registry.register(137, "SkinnedMeshRenderer");
|
||||
registry.register(138, "FixedJoint");
|
||||
registry.register(141, "BuildSettings");
|
||||
registry.register(143, "AssetBundle");
|
||||
registry.register(144, "CharacterController");
|
||||
registry.register(145, "CharacterJoint");
|
||||
registry.register(146, "SpringJoint");
|
||||
registry.register(147, "WheelCollider");
|
||||
registry.register(148, "ResourceManager");
|
||||
registry.register(149, "NetworkView");
|
||||
registry.register(150, "NetworkManager");
|
||||
registry.register(152, "MovieTexture");
|
||||
registry.register(153, "ConfigurableJoint");
|
||||
registry.register(154, "TerrainCollider");
|
||||
registry.register(155, "MasterServerInterface");
|
||||
registry.register(156, "TerrainData");
|
||||
registry.register(157, "LightmapSettings");
|
||||
registry.register(158, "WebCamTexture");
|
||||
registry.register(159, "EditorSettings");
|
||||
registry.register(162, "EditorUserSettings");
|
||||
registry.register(164, "AudioReverbFilter");
|
||||
registry.register(165, "AudioHighPassFilter");
|
||||
registry.register(166, "AudioChorusFilter");
|
||||
registry.register(167, "AudioReverbZone");
|
||||
registry.register(168, "AudioEchoFilter");
|
||||
registry.register(169, "AudioLowPassFilter");
|
||||
registry.register(170, "AudioDistortionFilter");
|
||||
registry.register(171, "SparseTexture");
|
||||
registry.register(180, "AudioBehaviour");
|
||||
registry.register(181, "AudioFilter");
|
||||
registry.register(182, "WindZone");
|
||||
registry.register(183, "Cloth");
|
||||
registry.register(184, "SubstanceArchive");
|
||||
registry.register(185, "ProceduralMaterial");
|
||||
registry.register(186, "ProceduralTexture");
|
||||
registry.register(191, "OffMeshLink");
|
||||
registry.register(192, "OcclusionArea");
|
||||
registry.register(193, "Tree");
|
||||
registry.register(194, "NavMeshObsolete");
|
||||
registry.register(195, "NavMeshAgent");
|
||||
registry.register(196, "NavMeshSettings");
|
||||
registry.register(197, "LightProbesLegacy");
|
||||
registry.register(198, "ParticleSystem");
|
||||
registry.register(199, "ParticleSystemRenderer");
|
||||
registry.register(200, "ShaderVariantCollection");
|
||||
registry.register(205, "LODGroup");
|
||||
registry.register(206, "BlendTree");
|
||||
registry.register(207, "Motion");
|
||||
registry.register(208, "NavMeshObstacle");
|
||||
registry.register(210, "TerrainInstance");
|
||||
registry.register(212, "SpriteRenderer");
|
||||
registry.register(213, "Sprite");
|
||||
registry.register(214, "CachedSpriteAtlas");
|
||||
registry.register(215, "ReflectionProbe");
|
||||
registry.register(218, "Terrain");
|
||||
registry.register(220, "LightProbeGroup");
|
||||
registry.register(221, "AnimatorOverrideController");
|
||||
registry.register(222, "CanvasRenderer");
|
||||
registry.register(223, "Canvas");
|
||||
registry.register(224, "RectTransform");
|
||||
registry.register(225, "CanvasGroup");
|
||||
registry.register(226, "BillboardAsset");
|
||||
registry.register(227, "BillboardRenderer");
|
||||
registry.register(228, "SpeedTreeWindAsset");
|
||||
registry.register(229, "AnchoredJoint2D");
|
||||
registry.register(230, "Joint2D");
|
||||
registry.register(231, "SpringJoint2D");
|
||||
registry.register(232, "DistanceJoint2D");
|
||||
registry.register(233, "HingeJoint2D");
|
||||
registry.register(234, "SliderJoint2D");
|
||||
registry.register(235, "WheelJoint2D");
|
||||
registry.register(238, "NavMeshData");
|
||||
registry.register(240, "AudioMixer");
|
||||
registry.register(241, "AudioMixerController");
|
||||
registry.register(243, "AudioMixerGroupController");
|
||||
registry.register(244, "AudioMixerEffectController");
|
||||
registry.register(245, "AudioMixerSnapshotController");
|
||||
registry.register(246, "PhysicsUpdateBehaviour2D");
|
||||
registry.register(247, "ConstantForce2D");
|
||||
registry.register(248, "Effector2D");
|
||||
registry.register(249, "AreaEffector2D");
|
||||
registry.register(250, "PointEffector2D");
|
||||
registry.register(251, "PlatformEffector2D");
|
||||
registry.register(252, "SurfaceEffector2D");
|
||||
registry.register(258, "LightProbes");
|
||||
registry.register(271, "SampleClip");
|
||||
registry.register(272, "AudioMixerSnapshot");
|
||||
registry.register(273, "AudioMixerGroup");
|
||||
registry.register(290, "AssetBundleManifest");
|
||||
registry.register(1001, "PrefabInstance");
|
||||
registry.register(1002, "EditorExtensionImpl");
|
||||
registry.register(1003, "AssetImporter");
|
||||
registry.register(1004, "AssetDatabase");
|
||||
registry.register(1005, "Mesh3DSImporter");
|
||||
registry.register(1006, "TextureImporter");
|
||||
registry.register(1007, "ShaderImporter");
|
||||
registry.register(1008, "ComputeShaderImporter");
|
||||
registry.register(1020, "AudioImporter");
|
||||
registry.register(1026, "HierarchyState");
|
||||
registry.register(1027, "GUIDSerializer");
|
||||
registry.register(1028, "AssetMetaData");
|
||||
registry.register(1029, "DefaultAsset");
|
||||
registry.register(1030, "DefaultImporter");
|
||||
registry.register(1031, "TextScriptImporter");
|
||||
registry.register(1032, "SceneAsset");
|
||||
registry.register(1034, "NativeFormatImporter");
|
||||
registry.register(1035, "MonoImporter");
|
||||
registry.register(1038, "LibraryAssetImporter");
|
||||
registry.register(1040, "ModelImporter");
|
||||
registry.register(1041, "FBXImporter");
|
||||
registry.register(1042, "TrueTypeFontImporter");
|
||||
registry.register(1045, "MovieImporter");
|
||||
registry.register(1050, "EditorBuildSettings");
|
||||
registry.register(1051, "DDSImporter");
|
||||
registry.register(1052, "InspectorExpandedState");
|
||||
registry.register(1053, "AnnotationManager");
|
||||
registry.register(1055, "MonoManager");
|
||||
registry.register(1101, "AnimatorStateMachine");
|
||||
registry.register(1102, "AnimatorState");
|
||||
registry.register(1105, "AnimatorStateTransition");
|
||||
registry.register(1107, "AnimatorTransition");
|
||||
|
||||
registry
|
||||
});
|
||||
|
||||
/// Get the class name for a Unity type ID
|
||||
///
|
||||
/// Returns None if the type ID is not registered.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use cursebreaker_parser::types::get_class_name;
|
||||
///
|
||||
/// assert_eq!(get_class_name(1), Some("GameObject"));
|
||||
/// assert_eq!(get_class_name(4), Some("Transform"));
|
||||
/// assert_eq!(get_class_name(999999), None);
|
||||
/// ```
|
||||
pub fn get_class_name(type_id: u32) -> Option<&'static str> {
|
||||
UNITY_TYPE_REGISTRY.get_class_name(type_id)
|
||||
}
|
||||
|
||||
/// Get the type ID for a Unity class name
|
||||
///
|
||||
/// Returns None if the class name is not registered.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use cursebreaker_parser::types::get_type_id;
|
||||
///
|
||||
/// assert_eq!(get_type_id("GameObject"), Some(1));
|
||||
/// assert_eq!(get_type_id("Transform"), Some(4));
|
||||
/// assert_eq!(get_type_id("UnknownType"), None);
|
||||
/// ```
|
||||
pub fn get_type_id(class_name: &str) -> Option<u32> {
|
||||
UNITY_TYPE_REGISTRY.get_type_id(class_name)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_class_name_game_object() {
|
||||
assert_eq!(get_class_name(1), Some("GameObject"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_class_name_transform() {
|
||||
assert_eq!(get_class_name(4), Some("Transform"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_class_name_rect_transform() {
|
||||
assert_eq!(get_class_name(224), Some("RectTransform"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_class_name_mono_behaviour() {
|
||||
assert_eq!(get_class_name(114), Some("MonoBehaviour"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_class_name_sprite_renderer() {
|
||||
assert_eq!(get_class_name(212), Some("SpriteRenderer"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_class_name_unknown() {
|
||||
assert_eq!(get_class_name(999999), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_type_id_game_object() {
|
||||
assert_eq!(get_type_id("GameObject"), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_type_id_transform() {
|
||||
assert_eq!(get_type_id("Transform"), Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_type_id_rect_transform() {
|
||||
assert_eq!(get_type_id("RectTransform"), Some(224));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_type_id_unknown() {
|
||||
assert_eq!(get_type_id("UnknownType"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bidirectional_mapping() {
|
||||
// Test that type_id -> name -> type_id works
|
||||
let type_id = 114;
|
||||
let class_name = get_class_name(type_id).unwrap();
|
||||
assert_eq!(class_name, "MonoBehaviour");
|
||||
assert_eq!(get_type_id(class_name), Some(type_id));
|
||||
}
|
||||
}
|
||||
83
tests/test_guid_resolution.rs
Normal file
83
tests/test_guid_resolution.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
//! 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