Compare commits

...

12 Commits

Author SHA1 Message Date
0f2cb68fa4 prefab spawning bugfix 2026-01-05 12:23:09 +00:00
dea28f5b9c stripped components 2026-01-05 10:18:35 +00:00
1867c5559b project load first 2026-01-05 08:12:55 +00:00
c58488c5fa colliders 2026-01-05 04:54:13 +00:00
71a031c3f9 unity components registration 2026-01-05 03:23:32 +00:00
16e83aca67 de-duplicate logger 2026-01-05 02:40:34 +00:00
b3f09bb742 removed examples 2026-01-04 14:17:32 +00:00
31662cda3b mesh renderer 2026-01-04 14:12:02 +00:00
37d6a9e6fc box collider 2026-01-04 14:05:36 +00:00
708cf77df1 renderer 2026-01-04 14:00:03 +00:00
c570e2b11a mesh filter 2026-01-04 13:59:56 +00:00
29813a46ef split project 2026-01-04 13:59:47 +00:00
42 changed files with 2073 additions and 2442 deletions

View File

@@ -12,7 +12,9 @@
"Bash(ls:*)",
"Bash(find:*)",
"Bash(grep:*)",
"Bash(wc:*)"
"Bash(wc:*)",
"Bash(pgrep:*)",
"Bash(cargo doc:*)"
],
"additionalDirectories": [
"/home/connor/repos/CBAssets/"

25
Cargo.lock generated
View File

@@ -23,6 +23,17 @@ 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 = [
"inventory",
"log",
"serde_yaml",
"sparsey",
"unity-parser",
]
[[package]]
name = "diff"
version = "0.1.13"
@@ -94,6 +105,12 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "lru"
version = "0.12.5"
@@ -242,6 +259,12 @@ dependencies = [
"unsafe-libyaml",
]
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "sparsey"
version = "0.13.3"
@@ -297,12 +320,14 @@ dependencies = [
"glam",
"indexmap",
"inventory",
"log",
"lru",
"once_cell",
"pretty_assertions",
"regex",
"serde",
"serde_yaml",
"smallvec",
"sparsey",
"thiserror",
"unity-parser-macros",

View File

@@ -1,5 +1,5 @@
[workspace]
members = ["unity-parser", "unity-parser-macros"]
members = ["unity-parser", "unity-parser-macros", "cursebreaker-parser"]
resolver = "2"
[workspace.package]

627
PREFAB_SUMMARY.md Normal file
View File

@@ -0,0 +1,627 @@
# Prefab Instantiation Deep Dive
This document explains how prefabs and nested prefabs are instantiated in the Unity Parser.
## Table of Contents
1. [Core Concepts](#core-concepts)
2. [Prefab Representation](#prefab-representation)
3. [Simple Prefab Instantiation](#simple-prefab-instantiation)
4. [Nested Prefab System](#nested-prefab-system)
5. [The 4-Pass ECS Building Process](#the-4-pass-ecs-building-process)
6. [FileID Remapping](#fileid-remapping)
7. [Code Examples](#code-examples)
---
## Core Concepts
### What is a Prefab?
In Unity, a **prefab** is a reusable template that can be instantiated multiple times in scenes. Think of it like a blueprint:
- A prefab file (`.prefab`) contains GameObjects and Components as YAML
- When placed in a scene, Unity creates an **instance** of that prefab
- Each instance can have **modifications** (overrides) applied to it
### What is a Nested Prefab?
A **nested prefab** is a prefab that contains instances of other prefabs within it. For example:
- `Player.prefab` might contain a nested `Weapon.prefab`
- When you instantiate `Player.prefab`, it must also instantiate `Weapon.prefab`
- This can go multiple levels deep
### Key Design Decision: Why Raw YAML?
Prefabs are stored as **raw YAML documents** (`UnityPrefab`) rather than fully-parsed ECS worlds (`UnityScene`). This is because:
1. **Efficient Cloning**: Prefabs need to be instantiated multiple times with different values
2. **YAML Overrides**: Unity stores modifications as YAML property path overrides (e.g., `m_LocalPosition.x = 100`)
3. **FileID Remapping**: Each instance needs unique FileIDs to avoid collisions
Scenes, on the other hand, are parsed directly into Sparsey ECS worlds since they don't need cloning.
---
## Prefab Representation
### UnityPrefab Structure
From `unity-parser/src/model/mod.rs:93-146`:
```rust
pub struct UnityPrefab {
/// Path to the prefab file
pub path: PathBuf,
/// Raw YAML documents that make up this prefab
pub documents: Vec<RawDocument>,
}
```
Each `RawDocument` contains:
- `type_id`: Unity type ID (e.g., 1 = GameObject, 4 = Transform)
- `file_id`: Unique identifier within the file
- `class_name`: The Unity class (e.g., "GameObject", "Transform", "MonoBehaviour")
- `yaml`: The raw YAML content as serde_yaml::Value
### Loading a Prefab
When you call `UnityFile::from_path("Player.prefab")`, the parser:
1. Reads the file content
2. Validates the Unity YAML header
3. Splits the YAML into separate documents (by `--- !u!N &ID` separators)
4. Creates `RawDocument` objects with metadata extracted
5. Returns `UnityFile::Prefab(UnityPrefab { path, documents })`
**Important**: At this stage, NO ECS world is created. The prefab stays as raw YAML.
---
## Simple Prefab Instantiation
### Step-by-Step Process
Let's walk through instantiating a simple prefab (no nesting):
#### 1. Create a PrefabInstance
From `unity-parser/src/types/unity_types/prefab_instance.rs:49-70`:
```rust
let prefab = UnityFile::from_path("Player.prefab")?;
let mut instance = prefab.instantiate();
```
`instantiate()` calls `PrefabInstance::new()` which:
- Clones all documents from the source prefab
- Initializes FileID remapping (creates new unique IDs)
- Remaps all FileID references in the YAML
#### 2. Apply Overrides (Optional)
You can modify the prefab before spawning:
```rust
instance.override_value(file_id, "m_Name", "Player1".into())?;
instance.override_value(file_id, "m_LocalPosition.x", 100.0.into())?;
```
This stores overrides in a HashMap that will be applied before spawning.
#### 3. Spawn into ECS World
```rust
let entities = instance.spawn_into(&mut world, &mut entity_map, guid_resolver, prefab_resolver)?;
```
`spawn_into()` (`prefab_instance.rs:291-309`):
1. Applies all stored overrides to the YAML
2. Calls `build_world_from_documents_into()` to create entities
3. Returns a Vec of spawned entities
### The Spawning Process
`build_world_from_documents_into()` from `unity-parser/src/ecs/builder.rs:160-265`:
**Pass 1**: Create entities for GameObjects
- Iterates through documents with `type_id == 1` (GameObject)
- Spawns ECS entities with `GameObject` component
- Adds to entity_map (FileID → Entity)
**Pass 2**: Attach components
- Iterates through remaining documents (Transform, RectTransform, MonoBehaviour, etc.)
- Looks up `m_GameObject` reference to find owner entity
- Parses and attaches component to entity
**Pass 3**: Execute deferred linking
- Resolves Transform parent/child relationships
- Converts FileID references to Entity handles
---
## Nested Prefab System
### How Unity Represents Nested Prefabs
When you place a prefab inside another prefab in Unity, it creates a **PrefabInstance** document:
```yaml
--- !u!1001 &1234567890
PrefabInstance:
m_SourcePrefab: {fileID: 0, guid: "091c537484687e9419460cdcd7038234", type: 3}
m_Modification:
- target: {fileID: 5678}
propertyPath: m_Name
value: "ModifiedName"
- target: {fileID: 5679}
propertyPath: m_LocalPosition.x
value: 10.5
```
### PrefabInstanceComponent
From `unity-parser/src/types/unity_types/prefab_instance.rs:322-366`:
```rust
pub struct PrefabInstanceComponent {
/// External reference to the source prefab (by GUID)
pub prefab_ref: ExternalRef, // Contains GUID string
/// Modifications applied to the nested prefab
pub modifications: Vec<PrefabModification>,
}
pub struct PrefabModification {
pub target_file_id: FileID, // Which object to modify
pub property_path: String, // Dot notation: "m_Name", "m_LocalPosition.x"
pub value: Value, // The new value
}
```
### GUID Resolution
Before we can instantiate a nested prefab, we need to resolve its GUID to a file path.
**PrefabGuidResolver** (`unity-parser/src/parser/prefab_guid_resolver.rs`):
1. **Initialization**: Scans Unity project directory for `.prefab.meta` files
2. **GUID Extraction**: Parses each `.meta` file to get the GUID
3. **Mapping**: Builds a HashMap: `Guid → PathBuf`
Example:
```
Assets/Prefabs/Player.prefab.meta contains:
guid: 091c537484687e9419460cdcd7038234
→ Maps: 0x091c537484687e9419460cdcd7038234 → "Assets/Prefabs/Player.prefab"
```
### PrefabResolver
**PrefabResolver** (`prefab_instance.rs:430-706`) handles loading and recursive instantiation:
```rust
pub struct PrefabResolver<'a> {
/// Cache of loaded prefabs (GUID → Prefab)
prefab_cache: HashMap<String, Arc<UnityPrefab>>,
/// Mapping from GUID to file path
guid_to_path: HashMap<String, PathBuf>,
/// Stack for circular reference detection
instantiation_stack: Vec<String>,
/// GUID resolver for MonoBehaviour scripts
guid_resolver: Option<&'a GuidResolver>,
/// Prefab GUID resolver
prefab_guid_resolver: Option<&'a PrefabGuidResolver>,
}
```
### Nested Prefab Instantiation Flow
From `prefab_instance.rs:496-572` - `instantiate_from_component()`:
```
PrefabInstanceComponent found in scene
1. Extract GUID from component.prefab_ref
2. Load prefab via GUID resolver: GUID → Path → UnityPrefab
3. Create PrefabInstance (clone + remap FileIDs)
4. Apply modifications from component.modifications
5. Spawn prefab into world (creates entities)
6. Link spawned root to parent entity (if provided)
Returns: Vec<Entity> of spawned entities
```
### Recursive Nested Prefabs
For deeply nested prefabs (prefabs containing prefabs containing prefabs...):
**instantiate_recursive()** (`prefab_instance.rs:574-643`):
```
Start with root prefab
1. Check for circular references (using instantiation_stack)
2. Push prefab ID to stack
3. Create PrefabInstance
4. Scan documents for nested PrefabInstance components
5. For each nested prefab:
- Load the referenced prefab by GUID
- Apply its modifications
- Recursively call instantiate_recursive()
6. Spawn this prefab's entities
7. Pop from stack
```
This handles arbitrary nesting depth while preventing infinite loops from circular references.
---
## The 4-Pass ECS Building Process
When parsing a Unity scene, the ECS builder uses a multi-pass approach to handle prefabs.
From `unity-parser/src/ecs/builder.rs:31-138`:
### Pass 1: Create GameObject Entities
```rust
for doc in documents.iter().filter(|d| d.type_id == 1 || d.class_name == "GameObject") {
let entity = spawn_game_object(&mut world, doc)?;
entity_map.insert(doc.file_id, entity);
}
// Also create entities for PrefabInstances
for doc in documents.iter().filter(|d| d.type_id == 1001 || d.class_name == "PrefabInstance") {
let entity = world.create(());
entity_map.insert(doc.file_id, entity);
// Parse and attach PrefabInstanceComponent
if let Some(prefab_comp) = PrefabInstanceComponent::parse(yaml, &ctx) {
world.insert(entity, (prefab_comp,));
}
}
```
At this stage:
- All GameObjects → Entities
- All PrefabInstances → Entities with `PrefabInstanceComponent` attached
- entity_map tracks FileID → Entity
### Pass 2: Attach Components
```rust
for doc in documents.iter().filter(|d| d.type_id != 1 && d.class_name != "GameObject") {
attach_component(&mut world, doc, &linking_ctx, &type_filter, guid_resolver)?;
}
```
- Parses Transform, RectTransform, MonoBehaviour, etc.
- Looks up `m_GameObject` reference to find owner entity
- Attaches parsed component to entity
### Pass 2.5: Resolve Prefab Instances (NEW!)
This is where the magic happens for nested prefabs (`builder.rs:92-132`):
```rust
if let Some(prefab_resolver_ref) = prefab_guid_resolver {
let mut prefab_resolver = PrefabResolver::from_resolvers(guid_resolver, prefab_resolver_ref);
// Query for entities with PrefabInstanceComponent
let prefab_entities: Vec<_> = world.query::<&PrefabInstanceComponent>().collect();
for (entity, component) in prefab_entities {
// Instantiate the referenced prefab
match prefab_resolver.instantiate_from_component(
&component,
Some(entity), // Parent entity
&mut world,
&mut entity_map,
) {
Ok(spawned) => {
info!("Spawned {} entities from prefab", spawned.len());
}
Err(e) => {
warn!("Failed to instantiate prefab: {}", e);
}
}
// Remove PrefabInstanceComponent after resolution
world.remove::<(PrefabInstanceComponent,)>(entity);
}
}
```
**Key Points**:
1. Only runs if a PrefabGuidResolver is provided
2. Finds all entities with `PrefabInstanceComponent`
3. For each one:
- Resolves GUID → loads prefab
- Creates instance with modifications
- Spawns into current world
- Links to parent entity
4. Removes `PrefabInstanceComponent` (no longer needed)
### Pass 3: Execute Deferred Linking
```rust
let entity_map = linking_ctx.execute_callbacks(&mut world);
```
- Resolves Transform parent/child relationships
- Converts FileID references to actual Entity handles
- This happens AFTER prefab instantiation so that prefab entities are in the map
---
## FileID Remapping
### Why Remap FileIDs?
Unity FileIDs are unique within a single file, but when instantiating multiple prefab instances, we need to ensure no collisions:
```
Scene.unity:
GameObject &100 ← FileID = 100
Transform &101 ← FileID = 101
Player.prefab (first instance):
GameObject &100 ← COLLISION!
Transform &200
Player.prefab (second instance):
GameObject &100 ← COLLISION!
Transform &200
```
### The Solution
From `prefab_instance.rs:72-114`:
**Step 1**: Generate unique IDs for each document
```rust
fn generate_file_id(&mut self) -> FileID {
let id = self.next_file_id; // Starts at i64::MAX
self.next_file_id -= 1; // Decrement
FileID::from_i64(id)
}
```
- Uses i64::MAX and decrements: `9223372036854775807`, `9223372036854775806`, ...
- Scene FileIDs are typically small positive numbers (1, 100, 1000)
- This avoids collisions
**Step 2**: Build mapping table
```rust
fn initialize_file_id_mapping(&mut self) {
for original_id in original_ids {
let new_id = self.generate_file_id();
self.file_id_map.insert(original_id, new_id);
}
}
```
Example mapping:
```
Original → New
100 → 9223372036854775807
200 → 9223372036854775806
```
**Step 3**: Remap all references
```rust
fn remap_yaml_file_refs(&mut self) {
// Update document's own FileID
for doc in &mut self.documents {
doc.file_id = self.file_id_map[&doc.file_id];
}
// Update all FileRef references in YAML: {fileID: N}
for doc in &mut self.documents {
Self::remap_value(&mut doc.yaml, &file_id_map);
}
}
```
This recursively walks the YAML tree and replaces all `{fileID: 100}` with `{fileID: 9223372036854775807}`.
### Handling Overrides
When applying overrides before spawning:
```rust
instance.override_value(original_file_id, "m_Name", "Player1".into())?;
```
The API accepts the **original** FileID for convenience, but internally:
```rust
fn apply_overrides(&mut self) -> Result<()> {
for ((file_id, path), value) in &self.overrides {
// Map original FileID → remapped FileID
let remapped_id = self.file_id_map.get(file_id)?;
// Find document with remapped ID
let doc = self.documents.iter_mut()
.find(|d| d.file_id == *remapped_id)?;
// Apply value change
set_yaml_value(&mut doc.yaml, path, value)?;
}
}
```
---
## Code Examples
### Example 1: Manual Prefab Instantiation
```rust
use unity_parser::{UnityFile, UnityPrefab};
use sparsey::World;
use std::collections::HashMap;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load prefab
let file = UnityFile::from_path("Assets/Prefabs/Player.prefab")?;
let prefab = match file {
UnityFile::Prefab(p) => p,
_ => panic!("Expected prefab"),
};
// Create instance with modifications
let mut instance = prefab.instantiate();
instance.override_value(file_id, "m_Name", "Player1".into())?;
instance.override_value(file_id, "m_LocalPosition.x", 100.0.into())?;
// Spawn into world
let mut world = World::new();
let mut entity_map = HashMap::new();
let entities = instance.spawn_into(&mut world, &mut entity_map, None, None)?;
println!("Spawned {} entities", entities.len());
Ok(())
}
```
### Example 2: Automatic Scene Parsing with Nested Prefabs
```rust
use unity_parser::UnityProject;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize project (builds GUID resolvers)
let project = UnityProject::from_path("/home/user/UnityProject")?;
// Parse scene - automatically resolves and instantiates prefabs
let scene = project.parse_scene("Assets/Scenes/Level1.unity")?;
println!("Scene has {} entities", scene.entity_map.len());
// Query entities
let game_objects = scene.world.borrow::<unity_parser::GameObject>();
let transforms = scene.world.borrow::<unity_parser::Transform>();
for (file_id, entity) in &scene.entity_map {
if let Some(go) = game_objects.get(*entity) {
if let Some(transform) = transforms.get(*entity) {
println!("GameObject '{}' at {:?}",
go.name(), transform.local_position());
}
}
}
Ok(())
}
```
### Example 3: Recursive Prefab Loading
```rust
use unity_parser::{UnityFile, PrefabResolver, PrefabGuidResolver};
use sparsey::World;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Build prefab GUID resolver
let prefab_guid_resolver = PrefabGuidResolver::from_project("UnityProject")?;
// Create prefab resolver
let mut prefab_resolver = PrefabResolver::from_resolvers(None, &prefab_guid_resolver);
// Load a prefab with nested prefabs
let file = UnityFile::from_path("Assets/Prefabs/ComplexPrefab.prefab")?;
let prefab = match file {
UnityFile::Prefab(p) => p,
_ => panic!("Expected prefab"),
};
// Recursively instantiate (handles nested prefabs automatically)
let mut world = World::new();
let mut entity_map = HashMap::new();
let entities = prefab_resolver.instantiate_recursive(
&prefab,
&mut world,
&mut entity_map,
)?;
println!("Recursively spawned {} entities", entities.len());
Ok(())
}
```
---
## Summary
### Prefab Instantiation Flow
```
UnityPrefab (raw YAML)
instantiate()
PrefabInstance (cloned YAML with remapped FileIDs)
override_value() (optional)
spawn_into()
ECS World (Sparsey entities with components)
```
### Nested Prefab Resolution Flow
```
Scene contains PrefabInstance document
Pass 1: Create entity with PrefabInstanceComponent
Pass 2.5: Find all PrefabInstanceComponent entities
For each: GUID → Path → Load Prefab
Create instance + apply modifications
Recursively check for nested PrefabInstances
Spawn all entities into world
Link to parent entity
```
### Key Takeaways
1. **Prefabs stay as YAML** until instantiation for efficient cloning and overrides
2. **FileID remapping** prevents collisions when instantiating multiple times
3. **PrefabGuidResolver** maps GUIDs to file paths for automatic loading
4. **Pass 2.5** in the ECS builder handles automatic prefab instantiation
5. **Recursive instantiation** handles arbitrary nesting depth with circular reference detection
6. **Modifications** are stored as property path + value pairs and applied before spawning
### Files to Explore
- `unity-parser/src/types/unity_types/prefab_instance.rs` - PrefabInstance, PrefabResolver
- `unity-parser/src/parser/prefab_guid_resolver.rs` - GUID → Path mapping
- `unity-parser/src/ecs/builder.rs` - 4-pass ECS building with prefab resolution
- `unity-parser/src/model/mod.rs` - UnityPrefab, UnityScene data structures

View File

@@ -253,78 +253,6 @@ Raw YAML Documents
- New: O(1) integer comparison
- **Significant speedup** for HashMap lookups
## Project Structure
```
cursebreaker-parser-rust/
├── unity-parser/ # Main library crate
│ ├── src/
│ │ ├── ecs/ # ECS world building
│ │ │ └── builder.rs
│ │ ├── model/ # UnityFile, Scene, Prefab, Asset
│ │ │ └── mod.rs
│ │ ├── parser/ # YAML parsing & GUID resolution
│ │ │ ├── guid_resolver.rs # Script GUID → Class Name
│ │ │ ├── prefab_guid_resolver.rs # Prefab GUID → Path
│ │ │ ├── meta.rs # .meta file parsing
│ │ │ ├── yaml.rs # YAML document splitting
│ │ │ └── mod.rs
│ │ ├── project/ # ⚠️ OUTDATED - needs refactoring
│ │ │ └── mod.rs
│ │ ├── types/ # Unity types & components
│ │ │ ├── unity_types/
│ │ │ │ ├── game_object.rs
│ │ │ │ ├── transform.rs
│ │ │ │ ├── prefab_instance.rs
│ │ │ │ └── mod.rs
│ │ │ ├── component.rs # UnityComponent trait & helpers
│ │ │ ├── guid.rs # 128-bit GUID type
│ │ │ ├── ids.rs # FileID, LocalID
│ │ │ ├── reference.rs # UnityReference enum
│ │ │ ├── type_filter.rs # TypeFilter for selective parsing
│ │ │ ├── values.rs # Vector3, Quaternion, Color, etc.
│ │ │ └── mod.rs
│ │ ├── error.rs # Error types
│ │ ├── macros.rs
│ │ ├── property/
│ │ └── lib.rs
│ ├── examples/
│ │ ├── basic_parsing.rs
│ │ ├── custom_component.rs
│ │ ├── ecs_integration.rs
│ │ ├── find_playsfx.rs
│ │ ├── parse_resources.rs
│ │ └── parse_resource_prefabs.rs
│ ├── tests/
│ └── Cargo.toml
├── unity-parser-macros/ # Proc macro crate (⚠️ has bugs)
│ ├── src/
│ │ └── lib.rs
│ └── Cargo.toml
├── Cargo.toml # Workspace config
└── README.md
```
## Known Issues
### Critical Issues
1. **`unity-parser/src/project/mod.rs` is OUTDATED**
- Built for old architecture before `UnityFile` enum refactor
- References non-existent `UnityDocument` type (should be `RawDocument`)
- Module is disabled in lib.rs until refactored
2. **Derive macro namespace mismatch**
- `unity-parser-macros` uses `unity_parser` namespace
- Actual crate name is `unity_parser::` (underscore, not hyphen)
- Manual `UnityComponent` implementation recommended
3. **Placeholder values in Cargo.toml**
- Author and repository fields need updating
### Minor Issues
1. Disabled example/test files may reference outdated APIs
2. Some examples may have incorrect YAML access patterns
## Running Examples
```bash
@@ -386,15 +314,6 @@ Contributions welcome! Areas needing help:
- **Testing**: Integration tests with real Unity projects
- **Performance**: Optimize YAML parsing, parallel processing
## License
Licensed under either of:
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
## Acknowledgments
- **Unity Technologies**: For the YAML-based file format

View File

@@ -0,0 +1,11 @@
[package]
name = "cursebreaker-parser"
version = "0.1.0"
edition = "2021"
[dependencies]
unity-parser = { path = "../unity-parser" }
serde_yaml = "0.9"
inventory = "0.3"
sparsey = "0.13"
log = { version = "0.4", features = ["std"] }

View File

@@ -6,61 +6,66 @@
//! 3. Extracting typeId and transform positions
//! 4. Writing resource data to an output file
use unity_parser::{UnityComponent, UnityFile};
mod types;
use types::InteractableResource;
use unity_parser::UnityProject;
use std::fs::File;
use std::io::Write;
use std::path::Path;
/// Interactable_Resource component from Cursebreaker
///
/// C# definition from Interactable_Resource.cs:
/// ```csharp
/// public class Interactable_Resource : Interactable
/// {
/// public int health;
/// public int maxHealth;
/// public int typeId;
/// // ... other fields
/// }
/// ```
#[derive(Debug, Clone, UnityComponent)]
#[unity_class("Interactable_Resource")]
pub struct InteractableResource {
#[unity_field("maxHealth")]
pub max_health: i64,
#[unity_field("typeId")]
pub type_id: i64,
}
use unity_parser::log::DedupLogger;
use log::{info, error, LevelFilter};
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("🎮 Cursebreaker - Resource Parser");
println!("{}", "=".repeat(70));
println!();
let scene_path = Path::new("/home/connor/repos/CBAssets/_GameAssets/Scenes/Tiles/10_3.unity");
let logger = DedupLogger::new();
log::set_boxed_logger(Box::new(logger))
.map(|()| log::set_max_level(LevelFilter::Trace))
.unwrap();
// log::set_max_level(LevelFilter::Warn);
// Check if scene exists
if !scene_path.exists() {
eprintln!("❌ Error: Scene not found at {}", scene_path.display());
return Err("Scene file not found".into());
}
info!("🎮 Cursebreaker - Resource Parser");
println!("📁 Parsing scene: {}", scene_path.display());
println!();
// Initialize Unity project once - scans entire project for GUID mappings
let project_root = Path::new("/home/connor/repos/CBAssets");
info!("📦 Initializing Unity project from: {}", project_root.display());
// Parse the scene
match UnityFile::from_path(&scene_path) {
Ok(UnityFile::Scene(scene)) => {
println!("✅ Scene parsed successfully!");
println!(" Total entities: {}", scene.entity_map.len());
println!();
let project = UnityProject::from_path(project_root)?;
// Now parse the scene using the pre-built GUID resolvers
let scene_path = "_GameAssets/Scenes/Tiles/10_3.unity";
info!("📁 Parsing scene: {}", scene_path);
log::logger().flush();
// Parse the scene using the project
match project.parse_scene(scene_path) {
Ok(scene) => {
info!("✅ Scene parsed successfully!");
info!(" Total entities: {}", scene.entity_map.len());
// Get views for component types we need
let resource_view = scene.world.borrow::<InteractableResource>();
let transform_view = scene.world.borrow::<unity_parser::Transform>();
let gameobject_view = scene.world.borrow::<unity_parser::GameObject>();
info!("DEBUG: entity_map size: {}", scene.entity_map.len());
info!("DEBUG: Checking world for InteractableResource components...");
// Check how many entities in entity_map have the component
let mut entities_in_map_with_resource = 0;
for entity in scene.entity_map.values() {
if resource_view.contains(*entity) {
entities_in_map_with_resource += 1;
if entities_in_map_with_resource <= 5 { // Only log first 5
if let Some(resource) = resource_view.get(*entity) {
info!("DEBUG: Entity {:?} in entity_map has InteractableResource, typeId: {}", entity, resource.type_id);
}
}
}
}
info!("DEBUG: Entities in entity_map with InteractableResource: {}", entities_in_map_with_resource);
// Find all entities that have Interactable_Resource
let mut found_resources = Vec::new();
@@ -81,21 +86,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
println!("🔍 Found {} Interactable_Resource component(s)", found_resources.len());
println!();
info!("🔍 Found {} Interactable_Resource component(s)", found_resources.len());
log::logger().flush();
if !found_resources.is_empty() {
// Display resources in console
for (name, resource, position) in &found_resources {
println!(" 📦 Resource: \"{}\"", name);
println!(" • typeId: {}", resource.type_id);
println!(" • maxHealth: {}", resource.max_health);
info!(" 📦 Resource: \"{}\"", name);
info!(" • typeId: {}", resource.type_id);
info!(" • maxHealth: {}", resource.max_health);
if let Some((x, y, z)) = position {
println!(" • Position: ({:.2}, {:.2}, {:.2})", x, y, z);
info!(" • Position: ({:.2}, {:.2}, {:.2})", x, y, z);
} else {
println!(" • Position: (no transform)");
info!(" • Position: (no transform)");
}
println!();
}
// Write to output file
@@ -125,23 +129,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
writeln!(output_file, "{}", "=".repeat(70))?;
writeln!(output_file, "End of resource data")?;
println!("📝 Resource data written to: {}", output_path);
println!();
info!("📝 Resource data written to: {}", output_path);
}
println!("{}", "=".repeat(70));
println!("✅ Parsing complete!");
println!("{}", "=".repeat(70));
}
Ok(_) => {
eprintln!("❌ Error: File is not a scene");
return Err("Not a Unity scene file".into());
info!("✅ Parsing complete!");
}
Err(e) => {
eprintln!("Parse error: {}", e);
error!("Parse error: {}", e);
return Err(Box::new(e));
}
}
log::logger().flush();
Ok(())
}

View File

@@ -0,0 +1,47 @@
/// Interactable_Resource component from Cursebreaker
///
/// C# definition from Interactable_Resource.cs:
/// ```csharp
/// public class Interactable_Resource : Interactable
/// {
/// public int health;
/// public int maxHealth;
/// public int typeId;
/// // ... other fields
/// }
/// ```
use unity_parser::{UnityComponent, ComponentContext, EcsInsertable};
use serde_yaml::Mapping;
#[derive(Debug, Clone)]
pub struct InteractableResource {
pub max_health: i64,
pub type_id: i64,
}
impl UnityComponent for InteractableResource {
fn parse(yaml: &Mapping, _ctx: &ComponentContext) -> Option<Self> {
Some(Self {
max_health: unity_parser::yaml_helpers::get_i64(yaml, "maxHealth").unwrap_or(0),
type_id: unity_parser::yaml_helpers::get_i64(yaml, "typeId").unwrap_or(0),
})
}
}
impl EcsInsertable for InteractableResource {
fn insert_into_world(self, world: &mut sparsey::World, entity: sparsey::Entity) {
world.insert(entity, (self,));
}
}
// Register component with inventory
inventory::submit! {
unity_parser::ComponentRegistration {
type_id: 114,
class_name: "Interactable_Resource",
parse_and_insert: |yaml, ctx, world, entity| {
<InteractableResource as EcsInsertable>::parse_and_insert(yaml, ctx, world, entity)
},
register: |builder| builder.register::<InteractableResource>(),
}
}

View File

@@ -0,0 +1,3 @@
mod interactable_resource;
pub use interactable_resource::InteractableResource;

View File

@@ -1,693 +0,0 @@
Cursebreaker Resource Prefabs
======================================================================
Total resources found: 171
----------------------------------------------------------------------
Prefab: HarvestableSpawner_73Medicinal Herbs
TypeID: 73
MaxHealth: 0
Prefab: HarvestableSpawner_65Radishes
TypeID: 65
MaxHealth: 0
Prefab: HarvestableSpawner_22Sandstone Grouper
TypeID: 22
MaxHealth: 0
Prefab: HarvestableSpawner_14Mageflower
TypeID: 14
MaxHealth: 0
Prefab: HarvestableSpawner_108mountain Ore3
TypeID: 108
MaxHealth: 0
Prefab: HarvestableSpawner_120Swamp Brambles
TypeID: 120
MaxHealth: 0
Prefab: HarvestableSpawner_20Caveshroom
TypeID: 20
MaxHealth: 0
Prefab: Prefab_Resource_30HeartbellVine
TypeID: 30
MaxHealth: 5
Prefab: HarvestableSpawner_76Speed herb
TypeID: 76
MaxHealth: 0
Prefab: Prefab_Resource_22SandstoneGrouper
TypeID: 22
MaxHealth: 5
Prefab: Prefab_Resource_64Carrot_Field
TypeID: 64
MaxHealth: 5
Prefab: HarvestableSpawner_26Betta Iotachi
TypeID: 26
MaxHealth: 0
Prefab: HarvestableSpawner_85Magic res ampoule crafting mat
TypeID: 85
MaxHealth: 0
Prefab: Prefab_Resource_15Blueberries
TypeID: 15
MaxHealth: 5
Prefab: HarvestableSpawner_58Radiant Sunflower
TypeID: 58
MaxHealth: 0
Prefab: HarvestableSpawner_76Waspheart Iris
TypeID: 76
MaxHealth: 0
Prefab: HarvestableSpawner_30Heartbell Vine
TypeID: 30
MaxHealth: 0
Prefab: HarvestableSpawner_61Eggplants
TypeID: 61
MaxHealth: 0
Prefab: Prefab_Resource_8Deadwood
TypeID: 0
MaxHealth: 5
Prefab: HarvestableSpawner_70Cabbage
TypeID: 70
MaxHealth: 0
Prefab: Prefab_Resource_19Coal
TypeID: 19
MaxHealth: 20
Prefab: Prefab_Resource_12PebbleSturgeon
TypeID: 12
MaxHealth: 5
Prefab: Prefab_Resource_6Oak
TypeID: 6
MaxHealth: 5
Prefab: HarvestableSpawner_2Copper Ore
TypeID: 2
MaxHealth: 0
Prefab: HarvestableSpawner_111Deep Earth Moss
TypeID: 111
MaxHealth: 0
Prefab: HarvestableSpawner_49Dreamylion
TypeID: 49
MaxHealth: 0
Prefab: HarvestableSpawner_71Nest
TypeID: 71
MaxHealth: 0
Prefab: HarvestableSpawner_19Coal
TypeID: 19
MaxHealth: 0
Prefab: Prefab_Resource_21CharcoalSnapper
TypeID: 21
MaxHealth: 5
Prefab: HarvestableSpawner_12Pebble Sturgeon
TypeID: 12
MaxHealth: 0
Prefab: Prefab_Resource_25ObsidianWalleye
TypeID: 25
MaxHealth: 5
Prefab: Prefab_Resource_65Radish_Field
TypeID: 65
MaxHealth: 5
Prefab: HarvestableSpawner_83Phys res ampoule crafting mat
TypeID: 83
MaxHealth: 0
Prefab: HarvestableSpawner_78Firestick flower
TypeID: 78
MaxHealth: 0
Prefab: Prefab_Resource_13ArcaneSeedTree
TypeID: 13
MaxHealth: 5
Prefab: HarvestableSpawner_116Tree
TypeID: 116
MaxHealth: 0
Prefab: HarvestableSpawner_6Oak
TypeID: 6
MaxHealth: 0
Prefab: HarvestableSpawner_37Ancients' Tears
TypeID: 37
MaxHealth: 0
Prefab: HarvestableSpawner_63Corn
TypeID: 63
MaxHealth: 0
Prefab: HarvestableSpawner_44Voidlight Tendril
TypeID: 44
MaxHealth: 0
Prefab: Prefab_Resource_5TitaniumOre
TypeID: 5
MaxHealth: 20
Prefab: HarvestableSpawner_55Poppy of the Fallen
TypeID: 55
MaxHealth: 0
Prefab: HarvestableSpawner_102mountain fish3
TypeID: 102
MaxHealth: 0
Prefab: HarvestableSpawner_15Blueberries
TypeID: 15
MaxHealth: 0
Prefab: HarvestableSpawner_41Falls Hydrangea
TypeID: 41
MaxHealth: 0
Prefab: Prefab_Resource_26BettaIotachi
TypeID: 26
MaxHealth: 5
Prefab: Prefab_Resource_36Noblerose
TypeID: 36
MaxHealth: 5
Prefab: Prefab_Resource_58Sunflower
TypeID: 58
MaxHealth: 5
Prefab: HarvestableSpawner_45Autumnleaf Lily
TypeID: 45
MaxHealth: 0
Prefab: Prefab_Resource_32Lakesberry
TypeID: 32
MaxHealth: 5
Prefab: HarvestableSpawner_106Imberite Ore
TypeID: 106
MaxHealth: 0
Prefab: HarvestableSpawner_100mountain fish1
TypeID: 100
MaxHealth: 0
Prefab: HarvestableSpawner_27Rock Lobster
TypeID: 27
MaxHealth: 0
Prefab: HarvestableSpawner_9Wild Goopy
TypeID: 9
MaxHealth: 0
Prefab: HarvestableSpawner_68Epic ore
TypeID: 68
MaxHealth: 0
Prefab: HarvestableSpawner_113Soulgrowth Mushrooms
TypeID: 113
MaxHealth: 0
Prefab: HarvestableSpawner_28Horsetail Reed Bass
TypeID: 28
MaxHealth: 0
Prefab: HarvestableSpawner_8Deadwood tree
TypeID: 8
MaxHealth: 0
Prefab: HarvestableSpawner_77Glowing Willow
TypeID: 77
MaxHealth: 0
Prefab: Prefab_Resource_33Silvermirror
TypeID: 33
MaxHealth: 5
Prefab: HarvestableSpawner_42Wight's Pearls
TypeID: 42
MaxHealth: 0
Prefab: HarvestableSpawner_16Spellbound Oak
TypeID: 16
MaxHealth: 0
Prefab: HarvestableSpawner_25Obsidian Walleye
TypeID: 25
MaxHealth: 0
Prefab: Prefab_Resource_50Haniflower
TypeID: 50
MaxHealth: 5
Prefab: Prefab_Resource_2CopperOre
TypeID: 2
MaxHealth: 20
Prefab: HarvestableSpawner_64Carrots
TypeID: 64
MaxHealth: 0
Prefab: HarvestableSpawner_32Lakesberry
TypeID: 32
MaxHealth: 0
Prefab: HarvestableSpawner_115Malicious Sporecap
TypeID: 115
MaxHealth: 0
Prefab: HarvestableSpawner_88Saltpeter Mine
TypeID: 88
MaxHealth: 0
Prefab: Prefab_Resource_23FuchsiaPerch
TypeID: 23
MaxHealth: 5
Prefab: Prefab_Resource_54GoldenSunflower
TypeID: 54
MaxHealth: 5
Prefab: HarvestableSpawner_58Sunflower
TypeID: 58
MaxHealth: 0
Prefab: HarvestableSpawner_117Ore
TypeID: 117
MaxHealth: 0
Prefab: HarvestableSpawner_1Spruce
TypeID: 1
MaxHealth: 0
Prefab: HarvestableSpawner_79Minigame tree
TypeID: 79
MaxHealth: 0
Prefab: HarvestableSpawner_7Evark tree
TypeID: 7
MaxHealth: 0
Prefab: HarvestableSpawner_81Puppet Flower
TypeID: 81
MaxHealth: 0
Prefab: HarvestableSpawner_6Oak tree
TypeID: 6
MaxHealth: 0
Prefab: Prefab_Resource_14Mageflower
TypeID: 14
MaxHealth: 5
Prefab: Prefab_Resource_7Evark
TypeID: 0
MaxHealth: 5
Prefab: HarvestableSpawner_86Generic mining
TypeID: 86
MaxHealth: 0
Prefab: HarvestableSpawner_85Darkmire Damp Root
TypeID: 85
MaxHealth: 0
Prefab: HarvestableSpawner_110Crystalvein Rose
TypeID: 110
MaxHealth: 0
Prefab: HarvestableSpawner_4Imberite ore
TypeID: 4
MaxHealth: 0
Prefab: Prefab_Resource_16SpellboundOak
TypeID: 16
MaxHealth: 5
Prefab: HarvestableSpawner_59Evening's Cup
TypeID: 59
MaxHealth: 0
Prefab: Prefab_Resource_38Dandelions
TypeID: 38
MaxHealth: 5
Prefab: HarvestableSpawner_101mountain fish2
TypeID: 101
MaxHealth: 0
Prefab: HarvestableSpawner_115MaliciousSporecap
TypeID: 115
MaxHealth: 0
Prefab: HarvestableSpawner_43Skyfold Flower
TypeID: 43
MaxHealth: 0
Prefab: HarvestableSpawner_67Cotton
TypeID: 67
MaxHealth: 0
Prefab: HarvestableSpawner_84Ranged res ampoule crafting mat
TypeID: 84
MaxHealth: 0
Prefab: HarvestableSpawner_23Fuchsia Perch
TypeID: 23
MaxHealth: 0
Prefab: HarvestableSpawner_84Marshland Feather Reed
TypeID: 84
MaxHealth: 0
Prefab: Prefab_Resource_24GoblinShad
TypeID: 24
MaxHealth: 5
Prefab: HarvestableSpawner_109mountain herb1
TypeID: 109
MaxHealth: 0
Prefab: HarvestableSpawner_35Red Nymph
TypeID: 35
MaxHealth: 0
Prefab: HarvestableSpawner_80Sunburst Marlin
TypeID: 80
MaxHealth: 0
Prefab: HarvestableSpawner_74Soothing Mallow
TypeID: 74
MaxHealth: 0
Prefab: HarvestableSpawner_60Golden Tulip
TypeID: 60
MaxHealth: 0
Prefab: HarvestableSpawner_114ColdwaterLily
TypeID: 114
MaxHealth: 0
Prefab: HarvestableSpawner_24Goblin Shad
TypeID: 24
MaxHealth: 0
Prefab: HarvestableSpawner_46Cloud Yarrow
TypeID: 46
MaxHealth: 0
Prefab: HarvestableSpawner_113SoulgrowthMushroom
TypeID: 113
MaxHealth: 0
Prefab: HarvestableSpawner_104mountain tree2
TypeID: 104
MaxHealth: 0
Prefab: Prefab_Resource_62Chili_Pepper_Field
TypeID: 62
MaxHealth: 5
Prefab: Prefab_Resource_67Cotton
TypeID: 67
MaxHealth: 5
Prefab: HarvestableSpawner_111mountain herb3
TypeID: 111
MaxHealth: 0
Prefab: Prefab_Resource_18Dust
TypeID: 18
MaxHealth: 5
Prefab: HarvestableSpawner_18Dust
TypeID: 18
MaxHealth: 0
Prefab: Prefab_Resource_1Spruce
TypeID: 1
MaxHealth: 5
Prefab: HarvestableSpawner_73Sweetgrass
TypeID: 73
MaxHealth: 0
Prefab: Prefab_Resource_35RedNymph
TypeID: 35
MaxHealth: 5
Prefab: HarvestableSpawner_72MinigameOre
TypeID: 72
MaxHealth: 0
Prefab: HarvestableSpawner_67Northwind Cotton
TypeID: 67
MaxHealth: 0
Prefab: HarvestableSpawner_57Queen's Lily
TypeID: 57
MaxHealth: 0
Prefab: HarvestableSpawner_103mountain tree1
TypeID: 103
MaxHealth: 0
Prefab: Prefab_Resource_3IronOre
TypeID: 3
MaxHealth: 20
Prefab: HarvestableSpawner_87Generic alchemy
TypeID: 87
MaxHealth: 0
Prefab: Prefab_Resource_31Bloodpetal
TypeID: 31
MaxHealth: 5
Prefab: HarvestableSpawner_1Spruce tree
TypeID: 1
MaxHealth: 0
Prefab: HarvestableSpawner_40Night's Bloom
TypeID: 40
MaxHealth: 0
Prefab: Prefab_Resource_28HorsetailReedBass
TypeID: 28
MaxHealth: 5
Prefab: HarvestableSpawner_11Redberries
TypeID: 11
MaxHealth: 0
Prefab: HarvestableSpawner_20Cavern Puffball
TypeID: 20
MaxHealth: 0
Prefab: HarvestableSpawner_37Ancient's Tears
TypeID: 37
MaxHealth: 0
Prefab: HarvestableSpawner_107mountain Ore2
TypeID: 107
MaxHealth: 0
Prefab: Prefab_Resource_27RockLobster
TypeID: 27
MaxHealth: 5
Prefab: Prefab_Resource_11RedberryBush
TypeID: 11
MaxHealth: 5
Prefab: HarvestableSpawner_62Chili Peppers
TypeID: 62
MaxHealth: 0
Prefab: HarvestableSpawner_106mountain Ore1
TypeID: 106
MaxHealth: 0
Prefab: HarvestableSpawner_56Deepforest Daisy
TypeID: 56
MaxHealth: 0
Prefab: HarvestableSpawner_105mountain tree3
TypeID: 105
MaxHealth: 0
Prefab: HarvestableSpawner_53Drakefire Root
TypeID: 53
MaxHealth: 0
Prefab: HarvestableSpawner_66Pumpkins
TypeID: 66
MaxHealth: 0
Prefab: Prefab_Resource_34SorcerersWeed
TypeID: 34
MaxHealth: 5
Prefab: HarvestableSpawner_112Rye
TypeID: 112
MaxHealth: 0
Prefab: HarvestableSpawner_3Iron ore
TypeID: 3
MaxHealth: 0
Prefab: Prefab_Resource_66Pumpkin_Patch
TypeID: 66
MaxHealth: 5
Prefab: HarvestableSpawner_73MedicinalHerbs
TypeID: 73
MaxHealth: 0
Prefab: HarvestableSpawner_33Silvermirror Iris
TypeID: 33
MaxHealth: 0
Prefab: HarvestableSpawner_69Cabbage
TypeID: 69
MaxHealth: 0
Prefab: HarvestableSpawner_13Arcane Everbloom
TypeID: 13
MaxHealth: 0
Prefab: Prefab_Resource_61Eggplant_Field
TypeID: 61
MaxHealth: 5
Prefab: HarvestableSpawner_48Sorrowleaf
TypeID: 48
MaxHealth: 0
Prefab: Prefab_Resource_63Corn_Field
TypeID: 63
MaxHealth: 5
Prefab: HarvestableSpawner_39Dawnflame Ivy
TypeID: 39
MaxHealth: 0
Prefab: HarvestableSpawner_54Golden Sunflower
TypeID: 54
MaxHealth: 0
Prefab: Prefab_Resource_37AncientsTears
TypeID: 37
MaxHealth: 5
Prefab: HarvestableSpawner_21Charcoal Snapper
TypeID: 21
MaxHealth: 0
Prefab: HarvestableSpawner_82Spider Egg
TypeID: 82
MaxHealth: 0
Prefab: HarvestableSpawner_34Sorcerer's Weed
TypeID: 34
MaxHealth: 0
Prefab: HarvestableSpawner_75Aurora Trout
TypeID: 75
MaxHealth: 0
Prefab: HarvestableSpawner_47Spellspore Cap
TypeID: 47
MaxHealth: 0
Prefab: HarvestableSpawner_109Skyfall Orchid
TypeID: 109
MaxHealth: 0
Prefab: HarvestableSpawner_52Shyflower Orchid
TypeID: 52
MaxHealth: 0
Prefab: HarvestableSpawner_110mountain herb2
TypeID: 110
MaxHealth: 0
Prefab: HarvestableSpawner_38Dandelions
TypeID: 38
MaxHealth: 0
Prefab: HarvestableSpawner_10Barley
TypeID: 10
MaxHealth: 0
Prefab: HarvestableSpawner_51Royal Daisy
TypeID: 51
MaxHealth: 0
Prefab: Prefab_Resource_20CaveShroom
TypeID: 20
MaxHealth: 20
Prefab: HarvestableSpawner_74Sven Herb 2
TypeID: 74
MaxHealth: 0
Prefab: HarvestableSpawner_69Potatoes
TypeID: 69
MaxHealth: 0
Prefab: HarvestableSpawner_114Coldwater Lily
TypeID: 114
MaxHealth: 0
Prefab: HarvestableSpawner_36Noblerose
TypeID: 36
MaxHealth: 0
Prefab: Prefab_Resource_10Barley_Field
TypeID: 10
MaxHealth: 5
Prefab: HarvestableSpawner_31Bloodpetal Rose
TypeID: 31
MaxHealth: 0
Prefab: Prefab_Resource_9WildGoopy
TypeID: 9
MaxHealth: 5
Prefab: HarvestableSpawner_83Gravelstem Whiteflower
TypeID: 83
MaxHealth: 0
Prefab: Prefab_Resource_41FallsHydrangea
TypeID: 41
MaxHealth: 5
Prefab: HarvestableSpawner_50Haniflower
TypeID: 50
MaxHealth: 0
======================================================================
End of resource data

View File

@@ -1,19 +0,0 @@
Cursebreaker Resources - 10_3.unity Scene
======================================================================
Total resources found: 2
----------------------------------------------------------------------
Resource: HarvestableSpawner_2Copper Ore
TypeID: 2
MaxHealth: 0
Position: (1788.727173, 40.725288, 172.017670)
Resource: HarvestableSpawner_38Dandelions
TypeID: 38
MaxHealth: 0
Position: (1746.709717, 44.599632, 299.696503)
======================================================================
End of resource data

View File

@@ -46,9 +46,15 @@ once_cell = "1.19"
# Component registry for custom MonoBehaviours
inventory = "0.3"
# Small vector optimization for materials list
smallvec = "1.13"
# Procedural macro for derive(UnityComponent)
unity-parser-macros = { path = "../unity-parser-macros" }
# Logging
log = "0.4"
[dev-dependencies]
# Testing utilities
pretty_assertions = "1.4"

View File

@@ -1,76 +0,0 @@
use unity_parser::UnityFile;
use std::path::Path;
fn main() {
// Parse a Unity prefab file
let prefab_path = Path::new("data/tests/unity-sampleproject/PiratePanic/Assets/PiratePanic/Prefabs/Menu/Battle/Hand/CardGrabber.prefab");
if !prefab_path.exists() {
eprintln!("Error: Unity sample project not found.");
eprintln!("Please ensure the git submodule is initialized:");
eprintln!(" git submodule update --init --recursive");
return;
}
// Parse the file
match UnityFile::from_path(prefab_path) {
Ok(file) => {
println!("Successfully parsed: {:?}", file.path().file_name().unwrap());
// Handle the different file types
match file {
UnityFile::Prefab(prefab) => {
println!("Found {} documents\n", prefab.documents.len());
// List all documents
for (i, doc) in prefab.documents.iter().enumerate() {
println!("Document {}: {} (Type ID: {}, File ID: {})",
i + 1,
doc.class_name,
doc.type_id,
doc.file_id
);
}
println!();
// Find all GameObjects
let game_objects = prefab.get_documents_by_class("GameObject");
println!("Found {} GameObjects:", game_objects.len());
for go in game_objects {
// doc.yaml already contains the inner content (after class wrapper)
if let Some(mapping) = go.as_mapping() {
if let Some(name) = mapping.get("m_Name").and_then(|v| v.as_str()) {
println!(" - {}", name);
}
}
}
println!();
// Find all Transforms
let transforms = prefab.get_documents_by_type(224); // RectTransform type ID
println!("Found {} RectTransforms", transforms.len());
// Look up a specific document by file ID
if let Some(first_doc) = prefab.documents.first() {
let file_id = first_doc.file_id;
if let Some(found) = prefab.get_document(file_id) {
println!("\nLooking up document by file ID {}:", file_id);
println!(" Class: {}", found.class_name);
}
}
}
UnityFile::Scene(scene) => {
println!("This is a scene file with {} entities", scene.entity_map.len());
}
UnityFile::Asset(asset) => {
println!("This is an asset file with {} documents", asset.documents.len());
}
}
}
Err(e) => {
eprintln!("Error parsing file: {}", e);
}
}
}

View File

@@ -1,101 +0,0 @@
//! Example demonstrating how to define custom Unity MonoBehaviour components
//! using the #[derive(UnityComponent)] macro.
use unity_parser::{yaml_helpers, ComponentContext, UnityComponent};
/// Custom Unity MonoBehaviour component for playing sound effects
///
/// This mirrors the C# PlaySFX MonoBehaviour:
/// ```csharp
/// public class PlaySFX : MonoBehaviour
/// {
/// [SerializeField] float volume;
/// [SerializeField] float startTime;
/// [SerializeField] float endTime;
/// [SerializeField] bool isLoop;
/// }
/// ```
#[derive(Debug, Clone, UnityComponent)]
#[unity_class("PlaySFX")]
pub struct PlaySFX {
#[unity_field("volume")]
pub volume: f64,
#[unity_field("startTime")]
pub start_time: f64,
#[unity_field("endTime")]
pub end_time: f64,
#[unity_field("isLoop")]
pub is_loop: bool,
}
/// Another example - a custom damage component
#[derive(Debug, Clone, UnityComponent)]
#[unity_class("DamageDealer")]
pub struct DamageDealer {
#[unity_field("damageAmount")]
pub damage_amount: f64,
#[unity_field("damageType")]
pub damage_type: String,
#[unity_field("canCrit")]
pub can_crit: bool,
#[unity_field("critMultiplier")]
pub crit_multiplier: f64,
}
fn main() {
println!("Custom Unity Component Example");
println!("===============================\n");
println!("Defined custom components:");
println!(" - PlaySFX: volume, start_time, end_time, is_loop");
println!(" - DamageDealer: damage_amount, damage_type, can_crit, crit_multiplier\n");
println!("These components are automatically registered via the inventory crate.");
println!("When parsing Unity files, they will be recognized and parsed automatically.\n");
// Demonstrate parsing from YAML
let yaml_str = r#"
volume: 0.75
startTime: 1.5
endTime: 3.0
isLoop: 1
"#;
let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap();
let mapping = yaml.as_mapping().unwrap();
// Create a dummy context
use unity_parser::{ComponentContext, FileID};
let ctx = ComponentContext {
type_id: 114,
file_id: FileID::from_i64(12345),
class_name: "PlaySFX",
entity: None,
linking_ctx: None,
yaml: mapping,
};
// Parse the component
if let Some(play_sfx) = PlaySFX::parse(mapping, &ctx) {
println!("Successfully parsed PlaySFX component:");
println!(" volume: {}", play_sfx.volume);
println!(" start_time: {}", play_sfx.start_time);
println!(" end_time: {}", play_sfx.end_time);
println!(" is_loop: {}", play_sfx.is_loop);
} else {
println!("Failed to parse PlaySFX component");
}
println!("\nTo use in your own code:");
println!(" 1. Define a struct matching your C# MonoBehaviour fields");
println!(" 2. Add #[derive(UnityComponent)] to the struct");
println!(" 3. Add #[unity_class(\"YourClassName\")] to specify the Unity class name");
println!(" 4. Add #[unity_field(\"fieldName\")] to each field");
println!(" 5. The component will be automatically registered and parsed!");
}

View File

@@ -1,146 +0,0 @@
//! Example demonstrating ECS integration and selective type parsing
//!
//! This example shows:
//! 1. Custom components being automatically inserted into the ECS world
//! 2. Using the parse_with_types! macro for selective parsing
//! 3. Querying the ECS world for components
use unity_parser::{parse_with_types, ComponentContext, EcsInsertable, FileID, TypeFilter, UnityComponent};
/// Custom Unity MonoBehaviour component
#[derive(Debug, Clone, UnityComponent)]
#[unity_class("PlaySFX")]
pub struct PlaySFX {
#[unity_field("volume")]
pub volume: f64,
#[unity_field("startTime")]
pub start_time: f64,
#[unity_field("endTime")]
pub end_time: f64,
#[unity_field("isLoop")]
pub is_loop: bool,
}
/// Another custom component
#[derive(Debug, Clone, UnityComponent)]
#[unity_class("Interactable")]
pub struct Interactable {
#[unity_field("interactionRadius")]
pub interaction_radius: f32,
#[unity_field("interactionText")]
pub interaction_text: String,
#[unity_field("canInteract")]
pub can_interact: bool,
}
fn main() {
println!("ECS Integration & Selective Parsing Example");
println!("{}", "=".repeat(60));
// Example 1: Using parse_with_types! macro
println!("\n1. Creating type filters:");
println!("{}", "-".repeat(60));
let _filter_all = TypeFilter::parse_all();
println!("✓ Filter that parses ALL types");
let filter_selective = parse_with_types! {
unity_types(Transform, Camera),
custom_types(PlaySFX)
};
println!("✓ Filter for Transform, Camera, and PlaySFX only");
let filter_custom_only = parse_with_types! {
custom_types(PlaySFX, Interactable)
};
println!("✓ Filter for PlaySFX and Interactable only (no Unity types)");
// Example 2: Demonstrating ECS insertion
println!("\n2. ECS Integration:");
println!("{}", "-".repeat(60));
// Simulate parsing a PlaySFX component
let yaml_str = r#"
volume: 0.8
startTime: 0.0
endTime: 5.0
isLoop: 0
"#;
let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap();
let mapping = yaml.as_mapping().unwrap();
let ctx = ComponentContext {
type_id: 114,
file_id: FileID::from_i64(12345),
class_name: "PlaySFX",
entity: None,
linking_ctx: None,
yaml: mapping,
};
// Parse the component
if let Some(play_sfx) = PlaySFX::parse(mapping, &ctx) {
println!("✓ Parsed PlaySFX component:");
println!(" - volume: {}", play_sfx.volume);
println!(" - start_time: {}", play_sfx.start_time);
println!(" - end_time: {}", play_sfx.end_time);
println!(" - is_loop: {}", play_sfx.is_loop);
// Create a minimal ECS world to demonstrate insertion
use sparsey::World;
let mut world = World::builder().register::<PlaySFX>().build();
let entity = world.create(());
println!("\n✓ Created ECS entity: {:?}", entity);
// Insert the component into the world
play_sfx.clone().insert_into_world(&mut world, entity);
println!("✓ Inserted PlaySFX component into ECS world");
// Query it back
{
let view = world.borrow::<PlaySFX>();
if let Some(component) = view.get(entity) {
println!("✓ Successfully queried component from ECS:");
println!(" - volume: {}", component.volume);
}
}
}
// Example 3: Type filter usage
println!("\n3. Type Filter Behavior:");
println!("{}", "-".repeat(60));
println!("Filter checks:");
println!(" Transform in selective filter: {}", filter_selective.should_parse_unity("Transform"));
println!(" Camera in selective filter: {}", filter_selective.should_parse_unity("Camera"));
println!(" Light in selective filter: {}", filter_selective.should_parse_unity("Light"));
println!(" PlaySFX in selective filter: {}", filter_selective.should_parse_custom("PlaySFX"));
println!(" Interactable in selective filter: {}", filter_selective.should_parse_custom("Interactable"));
println!("\n PlaySFX in custom-only filter: {}", filter_custom_only.should_parse_custom("PlaySFX"));
println!(" Transform in custom-only filter: {}", filter_custom_only.should_parse_unity("Transform"));
// Example 4: Benefits of selective parsing
println!("\n4. Benefits of Selective Parsing:");
println!("{}", "-".repeat(60));
println!("When parsing a large Unity project:");
println!(" • Parse ALL types: Parse everything (default)");
println!(" • Parse specific types: Faster parsing & less memory");
println!(" • Parse only what you need for your tool/analysis");
println!("\nExample use cases:");
println!(" • Animation tool: Only parse Animator, AnimationClip");
println!(" • Audio tool: Only parse AudioSource, PlaySFX");
println!(" • Transform analyzer: Only parse Transform, RectTransform");
println!();
println!("{}", "=".repeat(60));
println!("Complete! Custom components now work with ECS!");
println!("{}", "=".repeat(60));
}

View File

@@ -1,194 +0,0 @@
//! Demo: Find all PlaySFX components and their locations in VR_Horror_YouCantRun
//!
//! This example demonstrates:
//! 1. Parsing a real Unity project
//! 2. Finding custom MonoBehaviour components (PlaySFX)
//! 3. Querying the ECS world for components
//! 4. Accessing Transform data for component locations
use unity_parser::{UnityComponent, UnityFile};
use std::path::Path;
/// PlaySFX component from VR_Horror_YouCantRun
///
/// C# definition:
/// ```csharp
/// public class PlaySFX : MonoBehaviour
/// {
/// [SerializeField] float volume;
/// [SerializeField] float startTime;
/// [SerializeField] float endTime;
/// [SerializeField] bool isLoop;
/// }
/// ```
#[derive(Debug, Clone, UnityComponent)]
#[unity_class("PlaySFX")]
pub struct PlaySFX {
#[unity_field("volume")]
pub volume: f64,
#[unity_field("startTime")]
pub start_time: f64,
#[unity_field("endTime")]
pub end_time: f64,
#[unity_field("isLoop")]
pub is_loop: bool,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("🎮 VR Horror - PlaySFX Component Finder");
println!("{}", "=".repeat(70));
println!();
let project_path = Path::new("test_data/VR_Horror_YouCantRun");
// Check if project exists
if !project_path.exists() {
eprintln!("❌ Error: VR_Horror_YouCantRun project not found at {}", project_path.display());
eprintln!(" Run the integration tests first to download it:");
eprintln!(" cargo test test_vr_horror_project");
return Ok(());
}
println!("📁 Scanning project: {}", project_path.display());
println!();
// Find all Unity scene files
let scene_files = find_unity_files(project_path, "unity");
println!("📄 Found {} scene file(s)", scene_files.len());
println!();
let mut total_playsfx = 0;
// Parse each scene
for scene_path in scene_files {
println!("🔍 Parsing: {}", scene_path.file_name().unwrap().to_string_lossy());
match UnityFile::from_path(&scene_path) {
Ok(UnityFile::Scene(scene)) => {
// Get views for all component types we need
let playsfx_view = scene.world.borrow::<PlaySFX>();
let transform_view = scene.world.borrow::<unity_parser::Transform>();
let rect_transform_view = scene.world.borrow::<unity_parser::RectTransform>();
let gameobject_view = scene.world.borrow::<unity_parser::GameObject>();
// Find all entities that have PlaySFX
let mut found_count = 0;
let mut found_entities = Vec::new();
// Iterate through all entities in the entity_map
for entity in scene.entity_map.values() {
if let Some(playsfx) = playsfx_view.get(*entity) {
found_entities.push((*entity, playsfx.clone()));
found_count += 1;
}
}
if found_count > 0 {
println!(" ✅ Found {} PlaySFX component(s)", found_count);
total_playsfx += found_count;
// Process each found PlaySFX component
for (entity, playsfx) in found_entities {
let transform = transform_view.get(entity);
let rect_transform = rect_transform_view.get(entity);
let game_object = gameobject_view.get(entity);
let name = game_object
.and_then(|go| go.name())
.unwrap_or("(unnamed)");
println!();
println!(" 🔊 PlaySFX on GameObject: \"{}\"", name);
println!(" Entity: {:?}", entity);
println!(" Properties:");
println!(" • volume: {}", playsfx.volume);
println!(" • startTime: {}", playsfx.start_time);
println!(" • endTime: {}", playsfx.end_time);
println!(" • isLoop: {}", playsfx.is_loop);
// Print position if available
if let Some(transform) = transform {
if let Some(pos) = transform.local_position() {
println!(" Transform:");
println!(" • Position: ({:.2}, {:.2}, {:.2})",
pos.x, pos.y, pos.z);
}
if let Some(rot) = transform.local_rotation() {
println!(" • Rotation: ({:.2}, {:.2}, {:.2}, {:.2})",
rot.x, rot.y, rot.z, rot.w);
}
if let Some(scale) = transform.local_scale() {
println!(" • Scale: ({:.2}, {:.2}, {:.2})",
scale.x, scale.y, scale.z);
}
} else if let Some(rect_transform) = rect_transform {
let transform = rect_transform.transform();
if let Some(pos) = transform.local_position() {
println!(" RectTransform (UI):");
println!(" • Position: ({:.2}, {:.2}, {:.2})",
pos.x, pos.y, pos.z);
}
} else {
println!(" ⚠️ No Transform found");
}
}
} else {
println!(" ⊘ No PlaySFX components found");
}
println!();
}
Ok(_) => {
println!(" ⊘ Skipped (not a scene file)");
println!();
}
Err(e) => {
println!(" ❌ Parse error: {}", e);
println!();
}
}
}
println!("{}", "=".repeat(70));
println!("📊 Summary:");
println!(" Total PlaySFX components found: {}", total_playsfx);
println!("{}", "=".repeat(70));
Ok(())
}
/// Find all Unity files with a specific extension in a directory
fn find_unity_files(dir: &Path, extension: &str) -> Vec<std::path::PathBuf> {
let mut files = Vec::new();
fn visit_dir(dir: &Path, extension: &str, files: &mut Vec<std::path::PathBuf>) {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
// Skip Library, Temp, Builds, and .git directories
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if name == "Library" || name == "Temp" || name == "Builds" || name == ".git" {
continue;
}
}
if path.is_dir() {
visit_dir(&path, extension, files);
} else if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
if ext == extension {
files.push(path);
}
}
}
}
}
visit_dir(dir, extension, &mut files);
files.sort();
files
}

View File

@@ -1,218 +0,0 @@
//! Parse Cursebreaker Resource Prefabs
//!
//! This example demonstrates:
//! 1. Parsing Cursebreaker prefab files directly
//! 2. Finding Interactable_Resource components in prefabs
//! 3. Extracting typeId and maxHealth data
//! 4. Writing resource data to an output file
//!
//! Note: The 10_3.unity scene uses prefab instances, and the current parser
//! doesn't yet support resolving components from nested prefabs. This example
//! parses the prefab files directly instead.
use unity_parser::{GuidResolver, UnityComponent, UnityFile};
use std::fs::File;
use std::io::Write;
use std::path::Path;
use walkdir::WalkDir;
/// Interactable_Resource component from Cursebreaker
///
/// C# definition from Interactable_Resource.cs:
/// ```csharp
/// public class Interactable_Resource : Interactable
/// {
/// public int health;
/// public int maxHealth;
/// public int typeId;
/// // ... other fields
/// }
/// ```
#[derive(Debug, Clone, UnityComponent)]
#[unity_class("Interactable_Resource")]
pub struct InteractableResource {
#[unity_field("maxHealth")]
pub max_health: i64,
#[unity_field("typeId")]
pub type_id: i64,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("🎮 Cursebreaker - Resource Prefab Parser");
println!("{}", "=".repeat(70));
println!();
// Build GUID resolver for the project
let project_path = Path::new("/home/connor/repos/CBAssets");
println!("📦 Building GUID resolver for project: {}", project_path.display());
let resolver = match GuidResolver::from_project(project_path) {
Ok(r) => {
println!(" ✅ GUID resolver built successfully ({} mappings)", r.len());
// Debug: Check if we have Interactable_Resource
if let Some(class) = r.resolve_class_name("d39ddbf1c2c3d1a4baa070e5e76548bd") {
println!(" ✅ Found Interactable_Resource in resolver: {}", class);
} else {
println!(" ⚠️ Interactable_Resource NOT found in resolver");
// Try to find what we did find related to "Interactable"
println!(" Searching for similar class names...");
}
Some(r)
}
Err(e) => {
eprintln!(" ❌ Failed to build GUID resolver: {}", e);
None
}
};
println!();
let harvestables_dir = Path::new("/home/connor/repos/CBAssets/_GameAssets/Prefabs/Harvestables");
if !harvestables_dir.exists() {
eprintln!("❌ Error: Harvestables directory not found at {}", harvestables_dir.display());
return Err("Harvestables directory not found".into());
}
println!("📁 Scanning for harvestable prefabs in:");
println!(" {}", harvestables_dir.display());
println!();
// Find all prefab files
let mut prefab_files = Vec::new();
for entry in WalkDir::new(harvestables_dir)
.follow_links(false)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("prefab") {
prefab_files.push(path.to_path_buf());
}
}
println!("📄 Found {} prefab file(s)", prefab_files.len());
println!();
let mut all_resources = Vec::new();
// Parse each prefab using the GUID resolver we built
for prefab_path in &prefab_files {
println!("🔍 Parsing: {}", prefab_path.file_name().unwrap().to_string_lossy());
// For prefabs, we need to manually parse and check documents
// since prefabs don't have an ECS world like scenes do
match UnityFile::from_path(prefab_path) {
Ok(UnityFile::Prefab(prefab)) => {
// Search through YAML documents for Interactable_Resource components
let mut found_in_prefab = false;
for doc in &prefab.documents {
// Check if this document is a MonoBehaviour
if doc.class_name == "MonoBehaviour" {
// Try to extract the m_Script GUID
if let Some(m_script) = doc.yaml.get("m_Script").and_then(|v| v.as_mapping()) {
if let Some(guid_val) = m_script.get("guid").and_then(|v| v.as_str()) {
// Resolve GUID to class name
if let Some(ref res) = resolver {
if let Some(class_name) = res.resolve_class_name(guid_val) {
// Debug: print what we found
if prefab_path.file_name().unwrap().to_string_lossy().contains("Copper Ore") {
eprintln!("DEBUG: Found class '{}' in Copper Ore prefab", class_name);
}
if class_name == "Interactable_Resource" {
// Extract fields
let type_id = doc.yaml.get("typeId")
.and_then(|v| v.as_i64())
.unwrap_or(0);
let max_health = doc.yaml.get("maxHealth")
.and_then(|v| v.as_i64())
.unwrap_or(0);
let prefab_name = prefab_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
all_resources.push((
prefab_name.to_string(),
type_id,
max_health,
));
found_in_prefab = true;
}
} else if prefab_path.file_name().unwrap().to_string_lossy().contains("Copper Ore") {
eprintln!("DEBUG: Could not resolve GUID '{}' in Copper Ore prefab", guid_val);
}
}
}
}
}
}
if found_in_prefab {
println!(" ✅ Found Interactable_Resource");
} else {
println!(" ⊘ No Interactable_Resource found");
}
}
Ok(_) => {
println!(" ⊘ Not a prefab file");
}
Err(e) => {
println!(" ❌ Parse error: {}", e);
}
}
}
println!();
println!("{}", "=".repeat(70));
println!("📊 Summary: Found {} resource(s)", all_resources.len());
println!("{}", "=".repeat(70));
println!();
if !all_resources.is_empty() {
// Display resources
for (name, type_id, max_health) in &all_resources {
println!(" 📦 Prefab: \"{}\"", name);
println!(" • typeId: {}", type_id);
println!(" • maxHealth: {}", max_health);
println!();
}
// Write to output file
let output_path = "resource_prefabs_output.txt";
let mut output_file = File::create(output_path)?;
writeln!(output_file, "Cursebreaker Resource Prefabs")?;
writeln!(output_file, "{}", "=".repeat(70))?;
writeln!(output_file)?;
writeln!(output_file, "Total resources found: {}", all_resources.len())?;
writeln!(output_file)?;
writeln!(output_file, "{}", "-".repeat(70))?;
writeln!(output_file)?;
for (name, type_id, max_health) in &all_resources {
writeln!(output_file, "Prefab: {}", name)?;
writeln!(output_file, " TypeID: {}", type_id)?;
writeln!(output_file, " MaxHealth: {}", max_health)?;
writeln!(output_file)?;
}
writeln!(output_file, "{}", "=".repeat(70))?;
writeln!(output_file, "End of resource data")?;
println!("📝 Resource data written to: {}", output_path);
}
println!();
println!("{}", "=".repeat(70));
println!("✅ Parsing complete!");
println!("{}", "=".repeat(70));
Ok(())
}

View File

@@ -1,19 +0,0 @@
Cursebreaker Resources - 10_3.unity Scene
======================================================================
Total resources found: 2
----------------------------------------------------------------------
Resource: HarvestableSpawner_11Redberries
TypeID: 11
MaxHealth: 0
Position: (1769.135864, 32.664658, 150.395081)
Resource: HarvestableSpawner_38Dandelions
TypeID: 38
MaxHealth: 0
Position: (1746.709717, 44.599632, 299.696503)
======================================================================
End of resource data

View File

@@ -1,10 +1,12 @@
//! ECS world building from Unity documents
use log::{info, warn};
use crate::model::RawDocument;
use crate::parser::{GuidResolver, PrefabGuidResolver};
use crate::types::{
yaml_helpers, ComponentContext, FileID, GameObject, LinkingContext, PrefabInstanceComponent,
PrefabResolver, RectTransform, Transform, TypeFilter, UnityComponent,
PrefabResolver, RectTransform, StrippedReference, Transform, TypeFilter, UnityComponent,
};
use crate::{Error, Result};
use sparsey::{Entity, World};
@@ -37,7 +39,8 @@ pub fn build_world_from_documents(
.register::<GameObject>()
.register::<Transform>()
.register::<RectTransform>()
.register::<PrefabInstanceComponent>();
.register::<PrefabInstanceComponent>()
.register::<StrippedReference>();
// Register all custom components from inventory
for reg in inventory::iter::<crate::types::ComponentRegistration> {
@@ -78,9 +81,35 @@ pub fn build_world_from_documents(
}
}
// PASS 1.5: Handle stripped components
// Stripped components are references to prefab components that don't have full data in the scene
for doc in documents.iter().filter(|d| d.is_stripped) {
// Create an entity for this stripped reference
let entity = world.create(());
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
// Parse and attach the StrippedReference component
if let Some(yaml) = doc.as_mapping() {
let ctx = ComponentContext {
type_id: doc.type_id,
file_id: doc.file_id,
class_name: &doc.class_name,
entity: Some(entity),
linking_ctx: Some(&linking_ctx),
yaml,
guid_resolver,
};
if let Some(stripped_ref) = StrippedReference::parse(yaml, &ctx) {
world.insert(entity, (stripped_ref,));
}
}
}
// PASS 2: Attach components to entities
let type_filter = TypeFilter::parse_all();
for doc in documents.iter().filter(|d| {
!d.is_stripped &&
d.type_id != 1 && d.class_name != "GameObject" &&
d.type_id != 1001 && d.class_name != "PrefabInstance"
}) {
@@ -104,7 +133,7 @@ pub fn build_world_from_documents(
.collect();
drop(prefab_view); // Release the borrow
eprintln!("Pass 2.5: Found {} prefab instance(s) to resolve", prefab_entities.len());
info!("Pass 2.5: Found {} prefab instance(s) to resolve", prefab_entities.len());
for (entity, component) in prefab_entities {
match prefab_resolver.instantiate_from_component(
@@ -114,12 +143,12 @@ pub fn build_world_from_documents(
linking_ctx.borrow_mut().entity_map_mut(),
) {
Ok(spawned) => {
eprintln!("Spawned {} entities from prefab GUID: {}",
info!("Spawned {} entities from prefab GUID: {}",
spawned.len(), component.prefab_ref.guid);
}
Err(e) => {
// Soft failure - warn but continue
eprintln!(" ⚠️ Warning: Failed to instantiate prefab: {}", e);
warn!("Failed to instantiate prefab: {}", e);
}
}
@@ -132,6 +161,8 @@ pub fn build_world_from_documents(
// PASS 3: Execute all deferred linking callbacks
let entity_map = linking_ctx.into_inner().execute_callbacks(&mut world);
info!("DEBUG (build_world_from_documents): Final scene entity_map size: {}", entity_map.len());
Ok((world, entity_map))
}
@@ -202,9 +233,36 @@ pub fn build_world_from_documents_into(
}
}
// PASS 1.5: Handle stripped components
// Stripped components are references to prefab components that don't have full data in the scene
for doc in documents.iter().filter(|d| d.is_stripped) {
// Create an entity for this stripped reference
let entity = world.create(());
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
spawned_entities.push(entity);
// Parse and attach the StrippedReference component
if let Some(yaml) = doc.as_mapping() {
let ctx = ComponentContext {
type_id: doc.type_id,
file_id: doc.file_id,
class_name: &doc.class_name,
entity: Some(entity),
linking_ctx: Some(&linking_ctx),
yaml,
guid_resolver,
};
if let Some(stripped_ref) = StrippedReference::parse(yaml, &ctx) {
world.insert(entity, (stripped_ref,));
}
}
}
// PASS 2: Attach components to entities
let type_filter = TypeFilter::parse_all();
for doc in documents.iter().filter(|d| {
!d.is_stripped &&
d.type_id != 1 && d.class_name != "GameObject" &&
d.type_id != 1001 && d.class_name != "PrefabInstance"
}) {
@@ -229,7 +287,7 @@ pub fn build_world_from_documents_into(
drop(prefab_view); // Release the borrow
if !prefab_entities.is_empty() {
eprintln!("Pass 2.5 (nested): Found {} prefab instance(s) to resolve", prefab_entities.len());
info!("Pass 2.5 (nested): Found {} prefab instance(s) to resolve", prefab_entities.len());
}
for (entity, component) in prefab_entities {
@@ -240,11 +298,11 @@ pub fn build_world_from_documents_into(
linking_ctx.borrow_mut().entity_map_mut(),
) {
Ok(spawned) => {
eprintln!("Spawned {} entities from nested prefab", spawned.len());
info!("Spawned {} entities from nested prefab", spawned.len());
spawned_entities.extend(spawned);
}
Err(e) => {
eprintln!(" ⚠️ Warning: Failed to instantiate nested prefab: {}", e);
warn!("Failed to instantiate nested prefab: {}", e);
}
}
@@ -256,9 +314,14 @@ pub fn build_world_from_documents_into(
// PASS 3: Execute all deferred linking callbacks
let final_entity_map = linking_ctx.into_inner().execute_callbacks(world);
info!("DEBUG (build_world_from_documents_into): Spawned {} entities", spawned_entities.len());
info!("DEBUG (build_world_from_documents_into): final_entity_map size: {}, entity_map size before extend: {}", final_entity_map.len(), entity_map.len());
// Update caller's entity_map with new mappings
entity_map.extend(final_entity_map);
info!("DEBUG (build_world_from_documents_into): entity_map size after extend: {}", entity_map.len());
Ok(spawned_entities)
}
@@ -303,18 +366,34 @@ fn attach_component(
let go_ref = yaml_helpers::get_file_ref(yaml, "m_GameObject");
let entity = match go_ref {
Some(r) => linking_ctx
Some(r) => {
let entity_opt = linking_ctx
.borrow()
.entity_map()
.get(&r.file_id)
.copied()
.ok_or_else(|| {
Error::reference_error(format!("Unknown GameObject: {}", r.file_id))
})?,
.copied();
match entity_opt {
Some(e) => {
// Debug logging for Interactable_Resource MonoBehaviours
if doc.class_name == "MonoBehaviour" {
if let Some(script_ref) = yaml_helpers::get_external_ref(yaml, "m_Script") {
if script_ref.guid.as_str() == "d39ddbf1c2c3d1a4baa070e5e76548bd" {
info!("DEBUG: Found GameObject entity {:?} for Interactable_Resource MonoBehaviour (FileID: {})", e, doc.file_id);
}
}
}
e
}
None => {
return Err(Error::reference_error(format!("Unknown GameObject: {}", r.file_id)));
}
}
}
None => {
// Some components might not have m_GameObject (e.g., standalone assets)
eprintln!(
"Warning: Component {} has no m_GameObject reference",
warn!(
"Component {} has no m_GameObject reference",
doc.class_name
);
return Ok(());
@@ -368,6 +447,11 @@ fn attach_component(
if let Some(script_ref) = yaml_helpers::get_external_ref(yaml, "m_Script") {
// Resolve GUID to class name
if let Some(class_name) = resolver.resolve_class_name(script_ref.guid.as_str()) {
// Debug logging for specific GUID
if script_ref.guid.as_str() == "d39ddbf1c2c3d1a4baa070e5e76548bd" {
info!("DEBUG: Found Interactable_Resource GUID! Resolved to class name: {}", class_name);
}
// Try to find a registered custom component with this class name
let mut found_custom = false;
for reg in inventory::iter::<crate::types::ComponentRegistration> {
@@ -377,6 +461,7 @@ fn attach_component(
if (reg.parse_and_insert)(yaml, &ctx, world, entity) {
// Successfully parsed and inserted
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
info!("DEBUG: Successfully inserted component: {}", class_name);
}
break;
}
@@ -384,25 +469,25 @@ fn attach_component(
if !found_custom {
// GUID resolved but no registered component found
eprintln!(
"Warning: Skipping MonoBehaviour '{}' (no registered parser)",
warn!(
"Skipping MonoBehaviour '{}' (no registered parser)",
class_name
);
}
} else {
// GUID not found in resolver
eprintln!(
"Warning: Could not resolve MonoBehaviour GUID: {}",
warn!(
"Could not resolve MonoBehaviour GUID: {}",
script_ref.guid
);
}
} else {
// No m_Script reference found
eprintln!("Warning: MonoBehaviour missing m_Script reference");
warn!("MonoBehaviour missing m_Script reference");
}
} else {
// No GUID resolver available
eprintln!("Warning: Skipping MonoBehaviour (no GUID resolver available)");
warn!("Skipping MonoBehaviour (no GUID resolver available)");
}
}
_ => {
@@ -422,8 +507,8 @@ fn attach_component(
if !found_custom {
// Unknown component type - skip with warning
eprintln!(
"Warning: Skipping unknown component type: {}",
warn!(
"Skipping unknown component type: {}",
doc.class_name
);
}

View File

@@ -11,14 +11,14 @@
//! let file = UnityFile::from_path("Scene.unity")?;
//! match file {
//! UnityFile::Scene(scene) => {
//! println!("Scene with {} entities", scene.entity_map.len());
//! info!("Scene with {} entities", scene.entity_map.len());
//! // Access scene.world for ECS queries
//! }
//! UnityFile::Prefab(prefab) => {
//! println!("Prefab with {} documents", prefab.documents.len());
//! info!("Prefab with {} documents", prefab.documents.len());
//! }
//! UnityFile::Asset(asset) => {
//! println!("Asset with {} documents", asset.documents.len());
//! info!("Asset with {} documents", asset.documents.len());
//! }
//! }
//! # Ok::<(), unity_parser::Error>(())
@@ -27,6 +27,7 @@
// Public modules
pub mod ecs;
pub mod error;
pub mod log;
pub mod macros;
pub mod model;
pub mod parser;
@@ -37,13 +38,11 @@ pub mod types;
// Re-exports
pub use error::{Error, Result};
pub use model::{RawDocument, UnityAsset, UnityFile, UnityPrefab, UnityScene};
pub use model::{RawDocument, UnityAsset, UnityFile, UnityPrefab, UnityScene, UnityProject};
pub use parser::{
find_project_root, meta::MetaFile, parse_unity_file, parse_unity_file_filtered,
GuidResolver, PrefabGuidResolver,
};
// TODO: Re-enable once project module is updated
// pub use project::UnityProject;
pub use property::PropertyValue;
pub use types::{
get_class_name, get_type_id, Color, ComponentContext, ComponentRegistration, EcsInsertable,

View File

@@ -0,0 +1,107 @@
use log::{Level, Log, Metadata, Record};
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::SystemTime;
/// Entry storing deduplication information for a log message
#[derive(Debug, Clone)]
struct LogEntry {
count: usize,
last_logged: SystemTime,
level: Level,
}
/// A logger that deduplicates messages and batches output until flush is called
pub struct DedupLogger {
entries: Mutex<HashMap<String, LogEntry>>,
}
impl DedupLogger {
/// Create a new DedupLogger
pub fn new() -> Self {
Self {
entries: Mutex::new(HashMap::new()),
}
}
/// Flush all accumulated log messages to stdout, sorted by level then timestamp
pub fn flush(&self) {
let mut entries = self.entries.lock().unwrap();
if entries.is_empty() {
return;
}
// Convert HashMap to Vec for sorting
let mut messages: Vec<(String, LogEntry)> = entries
.drain()
.collect();
// Sort by level (descending: Trace > Debug > Info > Warn > Error)
// Then by timestamp (ascending: oldest first)
messages.sort_by(|a, b| {
let level_cmp = b.1.level.cmp(&a.1.level);
if level_cmp == std::cmp::Ordering::Equal {
a.1.last_logged.cmp(&b.1.last_logged)
} else {
level_cmp
}
});
// Print all messages
for (message, entry) in messages {
if entry.count == 1 {
println!("[{}] {}", entry.level, message);
} else {
println!("[{}] {} (x{})", entry.level, message, entry.count);
}
}
}
}
impl Default for DedupLogger {
fn default() -> Self {
Self::new()
}
}
impl Log for DedupLogger {
fn enabled(&self, _metadata: &Metadata) -> bool {
// Accept all log levels
true
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
let message = format!("{}", record.args());
let level = record.level();
let now = SystemTime::now();
let mut entries = self.entries.lock().unwrap();
entries
.entry(message)
.and_modify(|entry| {
entry.count += 1;
entry.last_logged = now;
// Update to highest severity level if it changed
if level < entry.level {
entry.level = level;
}
})
.or_insert(LogEntry {
count: 1,
last_logged: now,
level,
});
}
fn flush(&self) {
// The flush method in Log trait is called by the log crate
// We delegate to our custom flush implementation
self.flush();
}
}

View File

@@ -0,0 +1,3 @@
mod dedup_logger;
pub use dedup_logger::DedupLogger;

View File

@@ -4,10 +4,12 @@
//! Unity files can be Scenes (.unity), Prefabs (.prefab), or Assets (.asset), each
//! with different handling requirements.
use crate::parser::{GuidResolver, PrefabGuidResolver};
use crate::types::FileID;
use log::info;
use sparsey::{Entity, World};
use std::collections::HashMap;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
/// A parsed Unity file - can be a Scene, Prefab, or Asset
#[derive(Debug)]
@@ -134,12 +136,12 @@ impl UnityPrefab {
///
/// # Example
/// ```ignore
/// let mut instance = prefab.instantiate();
/// let mut instance = prefab.instantiate(None);
/// instance.override_value(file_id, "m_Name", "Player1".into())?;
/// let entities = instance.spawn_into(&mut world, &mut entity_map)?;
/// ```
pub fn instantiate(&self) -> crate::types::PrefabInstance {
crate::types::PrefabInstance::new(self)
pub fn instantiate(&self, file_id_counter: Option<std::sync::Arc<std::cell::Cell<i64>>>) -> crate::types::PrefabInstance {
crate::types::PrefabInstance::new(self, file_id_counter)
}
}
@@ -187,6 +189,9 @@ pub struct RawDocument {
/// Raw YAML value (inner mapping after class wrapper)
pub yaml: serde_yaml::Value,
/// Whether this component is stripped (exists in prefab, not in scene)
pub is_stripped: bool,
}
impl RawDocument {
@@ -196,12 +201,14 @@ impl RawDocument {
file_id: FileID,
class_name: String,
yaml: serde_yaml::Value,
is_stripped: bool,
) -> Self {
Self {
type_id,
file_id,
class_name,
yaml,
is_stripped,
}
}
@@ -210,3 +217,132 @@ impl RawDocument {
self.yaml.as_mapping()
}
}
/// A Unity project with pre-built GUID resolvers for efficient scene parsing
///
/// This struct holds pre-initialized GUID mappings for both MonoBehaviour scripts
/// and prefab files. By building these mappings once at initialization, you can
/// parse multiple scenes efficiently without rescanning the project for each file.
///
/// # Example
///
/// ```no_run
/// use unity_parser::UnityProject;
/// use std::path::Path;
///
/// // Initialize project once - scans entire project for .meta files
/// let project = UnityProject::from_path("/home/user/repos/CBAssets")?;
///
/// // Parse multiple scenes efficiently using pre-built resolvers
/// let scene1 = project.parse_scene("_GameAssets/Scenes/Tiles/10_3.unity")?;
/// let scene2 = project.parse_scene("_GameAssets/Scenes/Tiles/11_5.unity")?;
/// # Ok::<(), unity_parser::Error>(())
/// ```
#[derive(Debug, Clone)]
pub struct UnityProject {
/// Project root path
pub root: PathBuf,
/// Script GUID resolver (MonoBehaviour scripts)
pub guid_resolver: GuidResolver,
/// Prefab GUID resolver (Prefab files)
pub prefab_resolver: PrefabGuidResolver,
}
impl UnityProject {
/// Create a new UnityProject by scanning the entire project directory
///
/// This scans ALL subdirectories under the project root for .meta files
/// to build comprehensive GUID mappings for both scripts and prefabs.
///
/// Unlike the individual resolver constructors which only scan Assets/ or
/// _GameAssets/, this scans the entire project root to find assets in
/// directories like AssetDumpster/, AssetPacks/, etc.
///
/// # Arguments
///
/// * `root` - Path to the Unity project root directory
///
/// # Example
///
/// ```no_run
/// use unity_parser::UnityProject;
///
/// let project = UnityProject::from_path("/home/user/repos/CBAssets")?;
/// println!("Found {} script GUIDs", project.guid_resolver.len());
/// println!("Found {} prefab GUIDs", project.prefab_resolver.len());
/// # Ok::<(), unity_parser::Error>(())
/// ```
pub fn from_path(root: impl Into<PathBuf>) -> crate::Result<Self> {
let root = root.into();
info!("🔍 Initializing Unity project from: {}", root.display());
// Build script GUID resolver by scanning entire project root
let guid_resolver = GuidResolver::from_project_root(&root)?;
info!(" ✓ Script GUID resolver built ({} mappings)", guid_resolver.len());
// Build prefab GUID resolver by scanning entire project root
let prefab_resolver = PrefabGuidResolver::from_project_root(&root)?;
info!(" ✓ Prefab GUID resolver built ({} mappings)", prefab_resolver.len());
Ok(Self {
root,
guid_resolver,
prefab_resolver,
})
}
/// Parse a Unity scene using the pre-built GUID resolvers
///
/// This is more efficient than `UnityFile::from_path` when parsing multiple
/// scenes because the GUID resolvers are already initialized.
///
/// # Arguments
///
/// * `path` - Path to the scene file (relative to project root or absolute)
///
/// # Example
///
/// ```no_run
/// # use unity_parser::UnityProject;
/// # let project = UnityProject::from_path("/home/user/repos/CBAssets")?;
/// let scene = project.parse_scene("_GameAssets/Scenes/Tiles/10_3.unity")?;
/// println!("Scene has {} entities", scene.entity_map.len());
/// # Ok::<(), unity_parser::Error>(())
/// ```
pub fn parse_scene(&self, path: impl AsRef<Path>) -> crate::Result<UnityScene> {
let path = self.resolve_path(path.as_ref());
crate::parser::parse_scene_with_project(&path, self)
}
/// Parse a Unity prefab using the pre-built GUID resolvers
///
/// # Arguments
///
/// * `path` - Path to the prefab file (relative to project root or absolute)
pub fn parse_prefab(&self, path: impl AsRef<Path>) -> crate::Result<UnityPrefab> {
let path = self.resolve_path(path.as_ref());
crate::parser::parse_prefab_with_project(&path, self)
}
/// Parse a Unity asset file using the pre-built GUID resolvers
///
/// # Arguments
///
/// * `path` - Path to the asset file (relative to project root or absolute)
pub fn parse_asset(&self, path: impl AsRef<Path>) -> crate::Result<UnityAsset> {
let path = self.resolve_path(path.as_ref());
crate::parser::parse_asset_with_project(&path, self)
}
/// Resolve a path (if relative, make it relative to project root)
fn resolve_path(&self, path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
self.root.join(path)
}
}
}

View File

@@ -24,11 +24,13 @@
//! // Resolve a GUID to class name
//! let guid = "091c537484687e9419460cdcd7038234";
//! if let Some(class_name) = resolver.resolve_class_name(guid) {
//! println!("GUID {} → {}", guid, class_name);
//! info!("GUID {} → {}", guid, class_name);
//! }
//! # Ok::<(), unity_parser::Error>(())
//! ```
use log::warn;
use crate::parser::meta::MetaFile;
use crate::types::Guid;
use crate::{Error, Result};
@@ -64,6 +66,9 @@ impl GuidResolver {
/// parses them to extract GUIDs, then parses the corresponding
/// C# files to extract class names.
///
/// **Note**: This only scans the Assets/ or _GameAssets/ directory.
/// For scanning the entire project root, use `from_project_root`.
///
/// # Arguments
///
/// * `project_path` - Path to the Unity project root (containing Assets/ folder)
@@ -92,10 +97,49 @@ impl GuidResolver {
)));
};
Self::scan_directory(&assets_dir)
}
/// Build a GuidResolver by scanning the entire project root
///
/// Unlike `from_project` which only scans Assets/ or _GameAssets/,
/// this scans ALL subdirectories to find .cs.meta files. This is useful
/// when assets are spread across multiple directories (e.g., AssetDumpster/,
/// AssetPacks/, etc.).
///
/// # Arguments
///
/// * `project_root` - Path to the Unity project root directory
///
/// # Examples
///
/// ```no_run
/// use unity_parser::parser::GuidResolver;
/// use std::path::Path;
///
/// // Scans entire project root including AssetDumpster/, AssetPacks/, etc.
/// let resolver = GuidResolver::from_project_root(Path::new("/home/user/repos/CBAssets"))?;
/// # Ok::<(), unity_parser::Error>(())
/// ```
pub fn from_project_root(project_root: impl AsRef<Path>) -> Result<Self> {
let project_root = project_root.as_ref();
if !project_root.is_dir() {
return Err(Error::invalid_format(format!(
"Project root is not a directory: {}",
project_root.display()
)));
}
Self::scan_directory(project_root)
}
/// Internal method to scan a directory for .cs.meta files
fn scan_directory(dir: &Path) -> Result<Self> {
let mut resolver = Self::new();
// Walk the Assets directory looking for .cs.meta files
for entry in WalkDir::new(&assets_dir)
// Walk the directory looking for .cs.meta files
for entry in WalkDir::new(dir)
.follow_links(false)
.into_iter()
.filter_map(|e| e.ok())
@@ -111,7 +155,7 @@ impl GuidResolver {
let meta = match MetaFile::from_path(path) {
Ok(meta) => meta,
Err(e) => {
eprintln!("Warning: Failed to parse {}: {}", path.display(), e);
warn!("Failed to parse {}: {}", path.display(), e);
continue;
}
};
@@ -132,8 +176,8 @@ impl GuidResolver {
continue;
}
Err(e) => {
eprintln!(
"Warning: Failed to extract class name from {}: {}",
warn!(
"Failed to extract class name from {}: {}",
cs_path.display(),
e
);
@@ -145,8 +189,8 @@ impl GuidResolver {
let guid = match Guid::from_hex(meta.guid()) {
Ok(g) => g,
Err(e) => {
eprintln!(
"Warning: Invalid GUID in {}: {}",
warn!(
"Invalid GUID in {}: {}",
path.display(),
e
);
@@ -181,13 +225,13 @@ impl GuidResolver {
/// # let resolver = GuidResolver::from_project(Path::new("."))?;
/// // Resolve by string
/// if let Some(class_name) = resolver.resolve_class_name("091c537484687e9419460cdcd7038234") {
/// println!("Found class: {}", class_name);
/// info!("Found class: {}", class_name);
/// }
///
/// // Resolve by Guid
/// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234")?;
/// if let Some(class_name) = resolver.resolve_class_name(&guid) {
/// println!("Found class: {}", class_name);
/// info!("Found class: {}", class_name);
/// }
/// # Ok::<(), unity_parser::Error>(())
/// ```

View File

@@ -32,7 +32,7 @@ impl MetaFile {
/// use unity_parser::parser::meta::MetaFile;
///
/// let meta = MetaFile::from_path("Assets/Prefabs/Player.prefab.meta")?;
/// println!("GUID: {}", meta.guid);
/// info!("GUID: {}", meta.guid);
/// # Ok::<(), unity_parser::Error>(())
/// ```
pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {

View File

@@ -12,7 +12,9 @@ pub use prefab_guid_resolver::PrefabGuidResolver;
pub use unity_tag::{parse_unity_tag, UnityTag};
pub use yaml::split_yaml_documents;
use crate::model::{RawDocument, UnityAsset, UnityFile, UnityPrefab, UnityScene};
use log::{info, warn};
use crate::model::{RawDocument, UnityAsset, UnityFile, UnityPrefab, UnityScene, UnityProject};
use crate::types::{FileID, Guid, TypeFilter};
use crate::{Error, Result};
use regex::Regex;
@@ -148,16 +150,16 @@ fn parse_scene(path: &Path, content: &str, type_filter: Option<&TypeFilter>) ->
// Try to find Unity project root and build both GUID resolvers
let (guid_resolver, prefab_guid_resolver) = match find_project_root(path) {
Ok(project_root) => {
eprintln!("📦 Found Unity project root: {}", project_root.display());
info!("📦 Found Unity project root: {}", project_root.display());
// Build script GUID resolver
let guid_res = match GuidResolver::from_project(&project_root) {
Ok(resolver) => {
eprintln!("Script GUID resolver built ({} mappings)", resolver.len());
info!("Script GUID resolver built ({} mappings)", resolver.len());
Some(resolver)
}
Err(e) => {
eprintln!(" ⚠️ Warning: Failed to build script GUID resolver: {}", e);
warn!("Failed to build script GUID resolver: {}", e);
None
}
};
@@ -165,11 +167,11 @@ fn parse_scene(path: &Path, content: &str, type_filter: Option<&TypeFilter>) ->
// Build prefab GUID resolver
let prefab_res = match PrefabGuidResolver::from_project(&project_root) {
Ok(resolver) => {
eprintln!("Prefab GUID resolver built ({} mappings)", resolver.len());
info!("Prefab GUID resolver built ({} mappings)", resolver.len());
Some(resolver)
}
Err(e) => {
eprintln!(" ⚠️ Warning: Failed to build prefab GUID resolver: {}", e);
warn!("Failed to build prefab GUID resolver: {}", e);
None
}
};
@@ -196,6 +198,83 @@ fn parse_scene(path: &Path, content: &str, type_filter: Option<&TypeFilter>) ->
)))
}
/// Parse a scene file using pre-built GUID resolvers from a UnityProject
///
/// This is more efficient than `parse_scene` when parsing multiple scenes
/// because the GUID resolvers are already initialized.
///
/// # Arguments
///
/// * `path` - Path to the scene file
/// * `project` - Pre-initialized UnityProject with GUID resolvers
pub fn parse_scene_with_project(path: &Path, project: &UnityProject) -> Result<UnityScene> {
// Read the file
let content = std::fs::read_to_string(path)?;
// Validate Unity header
validate_unity_header(&content, path)?;
// Parse raw documents
let raw_documents = parse_raw_documents(&content, None)?;
// Build ECS world from documents using project's resolvers
let (world, entity_map) = crate::ecs::build_world_from_documents(
raw_documents,
Some(&project.guid_resolver),
Some(&project.prefab_resolver),
)?;
Ok(UnityScene::new(
path.to_path_buf(),
world,
entity_map,
))
}
/// Parse a prefab file using pre-built GUID resolvers from a UnityProject
///
/// # Arguments
///
/// * `path` - Path to the prefab file
/// * `project` - Pre-initialized UnityProject with GUID resolvers
pub fn parse_prefab_with_project(path: &Path, _project: &UnityProject) -> Result<UnityPrefab> {
// Read the file
let content = std::fs::read_to_string(path)?;
// Validate Unity header
validate_unity_header(&content, path)?;
// Parse raw documents
let raw_documents = parse_raw_documents(&content, None)?;
Ok(UnityPrefab::new(
path.to_path_buf(),
raw_documents,
))
}
/// Parse an asset file using pre-built GUID resolvers from a UnityProject
///
/// # Arguments
///
/// * `path` - Path to the asset file
/// * `project` - Pre-initialized UnityProject with GUID resolvers
pub fn parse_asset_with_project(path: &Path, _project: &UnityProject) -> Result<UnityAsset> {
// Read the file
let content = std::fs::read_to_string(path)?;
// Validate Unity header
validate_unity_header(&content, path)?;
// Parse raw documents
let raw_documents = parse_raw_documents(&content, None)?;
Ok(UnityAsset::new(
path.to_path_buf(),
raw_documents,
))
}
/// Parse a prefab file into raw YAML documents
fn parse_prefab(path: &Path, content: &str, type_filter: Option<&TypeFilter>) -> Result<UnityFile> {
let raw_documents = parse_raw_documents(content, type_filter)?;
@@ -305,6 +384,7 @@ fn parse_raw_document(raw_doc: &str, type_filter: Option<&TypeFilter>) -> Result
FileID::from_i64(tag.file_id),
class_name,
inner_yaml,
tag.is_stripped,
)))
}

View File

@@ -28,6 +28,8 @@
//! # Ok::<(), unity_parser::Error>(())
//! ```
use log::warn;
use crate::parser::meta::MetaFile;
use crate::types::Guid;
use crate::{Error, Result};
@@ -60,6 +62,9 @@ impl PrefabGuidResolver {
/// This scans for all `.prefab.meta` files and extracts their GUIDs
/// to build a GUID → Prefab Path mapping.
///
/// **Note**: This only scans the Assets/ or _GameAssets/ directory.
/// For scanning the entire project root, use `from_project_root`.
///
/// # Arguments
///
/// * `project_path` - Path to the Unity project root (containing Assets/ folder)
@@ -88,10 +93,49 @@ impl PrefabGuidResolver {
)));
};
Self::scan_directory(&assets_dir)
}
/// Build a PrefabGuidResolver by scanning the entire project root
///
/// Unlike `from_project` which only scans Assets/ or _GameAssets/,
/// this scans ALL subdirectories to find .prefab.meta files. This is useful
/// when prefabs are spread across multiple directories (e.g., AssetDumpster/,
/// AssetPacks/, etc.).
///
/// # Arguments
///
/// * `project_root` - Path to the Unity project root directory
///
/// # Examples
///
/// ```no_run
/// use unity_parser::parser::PrefabGuidResolver;
/// use std::path::Path;
///
/// // Scans entire project root including AssetDumpster/, AssetPacks/, etc.
/// let resolver = PrefabGuidResolver::from_project_root(Path::new("/home/user/repos/CBAssets"))?;
/// # Ok::<(), unity_parser::Error>(())
/// ```
pub fn from_project_root(project_root: impl AsRef<Path>) -> Result<Self> {
let project_root = project_root.as_ref();
if !project_root.is_dir() {
return Err(Error::invalid_format(format!(
"Project root is not a directory: {}",
project_root.display()
)));
}
Self::scan_directory(project_root)
}
/// Internal method to scan a directory for .prefab.meta files
fn scan_directory(dir: &Path) -> Result<Self> {
let mut resolver = Self::new();
// Walk the Assets directory looking for .prefab.meta files
for entry in WalkDir::new(&assets_dir)
// Walk the directory looking for .prefab.meta files
for entry in WalkDir::new(dir)
.follow_links(false)
.into_iter()
.filter_map(|e| e.ok())
@@ -107,7 +151,7 @@ impl PrefabGuidResolver {
let meta = match MetaFile::from_path(path) {
Ok(meta) => meta,
Err(e) => {
eprintln!("Warning: Failed to parse {}: {}", path.display(), e);
warn!("Failed to parse {}: {}", path.display(), e);
continue;
}
};
@@ -124,8 +168,8 @@ impl PrefabGuidResolver {
let guid = match Guid::from_hex(meta.guid()) {
Ok(g) => g,
Err(e) => {
eprintln!(
"Warning: Invalid GUID in {}: {}",
warn!(
"Invalid GUID in {}: {}",
path.display(),
e
);

View File

@@ -15,15 +15,19 @@ pub struct UnityTag {
/// File ID (the number after &)
pub file_id: i64,
/// Whether this component is stripped (exists in prefab, not in scene)
pub is_stripped: bool,
}
/// Get the Unity tag regex (compiled once and cached)
fn unity_tag_regex() -> &'static Regex {
static REGEX: OnceLock<Regex> = OnceLock::new();
REGEX.get_or_init(|| {
// Matches: --- !u!<type_id> &<file_id>
// Matches: --- !u!<type_id> &<file_id> [stripped]
// Example: --- !u!1 &1866116814460599870
Regex::new(r"^---\s+!u!(\d+)\s+&(-?\d+)").unwrap()
// Example: --- !u!4 &104494228 stripped
Regex::new(r"^---\s+!u!(\d+)\s+&(-?\d+)(?:\s+(stripped))?").unwrap()
})
}
@@ -38,6 +42,7 @@ fn unity_tag_regex() -> &'static Regex {
/// let tag = parse_unity_tag(doc).unwrap();
/// assert_eq!(tag.type_id, 1);
/// assert_eq!(tag.file_id, 12345);
/// assert_eq!(tag.is_stripped, false);
/// ```
pub fn parse_unity_tag(document: &str) -> Option<UnityTag> {
let re = unity_tag_regex();
@@ -52,7 +57,14 @@ pub fn parse_unity_tag(document: &str) -> Option<UnityTag> {
let type_id = captures.get(1)?.as_str().parse::<u32>().ok()?;
let file_id = captures.get(2)?.as_str().parse::<i64>().ok()?;
Some(UnityTag { type_id, file_id })
// Check if "stripped" keyword is present
let is_stripped = captures.get(3).is_some();
Some(UnityTag {
type_id,
file_id,
is_stripped,
})
}
#[cfg(test)]
@@ -65,6 +77,7 @@ mod tests {
let tag = parse_unity_tag(doc).unwrap();
assert_eq!(tag.type_id, 1);
assert_eq!(tag.file_id, 1866116814460599870);
assert_eq!(tag.is_stripped, false);
}
#[test]
@@ -73,6 +86,7 @@ mod tests {
let tag = parse_unity_tag(doc).unwrap();
assert_eq!(tag.type_id, 224);
assert_eq!(tag.file_id, 8151827567463220614);
assert_eq!(tag.is_stripped, false);
}
#[test]
@@ -81,6 +95,16 @@ mod tests {
let tag = parse_unity_tag(doc).unwrap();
assert_eq!(tag.type_id, 114);
assert_eq!(tag.file_id, -12345);
assert_eq!(tag.is_stripped, false);
}
#[test]
fn test_parse_unity_tag_stripped() {
let doc = "--- !u!4 &104494228 stripped\nTransform:\n m_CorrespondingSourceObject: {fileID: 1381906096329791709}";
let tag = parse_unity_tag(doc).unwrap();
assert_eq!(tag.type_id, 4);
assert_eq!(tag.file_id, 104494228);
assert_eq!(tag.is_stripped, true);
}
#[test]

View File

@@ -23,7 +23,7 @@ pub use reference::UnityReference;
pub use type_filter::TypeFilter;
pub use type_registry::{get_class_name, get_type_id};
pub use unity_types::{
GameObject, PrefabInstance, PrefabInstanceComponent, PrefabModification, PrefabResolver,
RectTransform, Transform,
BoxCollider, GameObject, MeshFilter, MeshRenderer, PrefabInstance, PrefabInstanceComponent,
PrefabModification, PrefabResolver, RectTransform, Renderer, StrippedReference, Transform,
};
pub use values::{Color, ExternalRef, FileRef, Quaternion, Vector2, Vector3};

View File

@@ -0,0 +1,87 @@
//! BoxCollider component wrapper
use crate::types::{yaml_helpers, ComponentContext, UnityComponent, Vector3};
use crate::types::unity_types::Collider;
use sparsey::Entity;
/// A BoxCollider component
///
/// BoxCollider is a basic collision primitive in the shape of a box.
/// It defines a box-shaped volume that can be used for collision detection.
#[derive(Debug, Clone)]
pub struct BoxCollider {
size: Vector3,
center: Vector3,
collider: Collider,
}
impl BoxCollider {
/// Get the size of the box collider
pub fn size(&self) -> &Vector3 {
&self.size
}
/// Get the center offset of the box collider
pub fn center(&self) -> &Vector3 {
&self.center
}
/// Get the base collider component
pub fn collider(&self) -> &Collider {
&self.collider
}
/// Get mutable access to the base collider component
pub fn collider_mut(&mut self) -> &mut Collider {
&mut self.collider
}
/// Set the size of the box collider
pub fn set_size(&mut self, size: Vector3) {
self.size = size;
}
/// Set the center offset of the box collider
pub fn set_center(&mut self, center: Vector3) {
self.center = center;
}
}
impl UnityComponent for BoxCollider {
/// Parse a BoxCollider from YAML
///
/// Note: Caller is responsible for ensuring this is called on the correct document type.
fn parse(yaml: &serde_yaml::Mapping, ctx: &ComponentContext) -> Option<Self> {
let size = yaml_helpers::get_vector3(yaml, "m_Size").unwrap_or_default();
let center = yaml_helpers::get_vector3(yaml, "m_Center").unwrap_or_default();
// Parse the base collider component (layer masks)
let collider = Collider::parse(yaml, ctx)?;
Some(Self { size, center, collider })
}
}
impl crate::types::EcsInsertable for BoxCollider {
fn insert_into_world(self, world: &mut sparsey::World, entity: Entity) {
// Insert both the BoxCollider and the base Collider component
// This allows querying for either specific collider types or all colliders
let collider = self.collider.clone();
world.insert(entity, (self, collider));
}
}
// Register component with inventory
inventory::submit! {
crate::types::ComponentRegistration {
type_id: 65,
class_name: "BoxCollider",
parse_and_insert: |yaml, ctx, world, entity| {
<BoxCollider as crate::types::EcsInsertable>::parse_and_insert(yaml, ctx, world, entity)
},
register: |builder| {
// Register both BoxCollider and the base Collider component
builder.register::<BoxCollider>().register::<Collider>()
},
}
}

View File

@@ -0,0 +1,117 @@
//! CapsuleCollider component wrapper
use crate::types::{yaml_helpers, ComponentContext, UnityComponent, Vector3};
use crate::types::unity_types::Collider;
use sparsey::Entity;
/// A CapsuleCollider component
///
/// CapsuleCollider is a basic collision primitive in the shape of a capsule.
/// It defines a capsule-shaped volume (cylinder with hemispherical ends) that can be used for collision detection.
#[derive(Debug, Clone)]
pub struct CapsuleCollider {
radius: f32,
height: f32,
center: Vector3,
direction: i32, // 0 = X-axis, 1 = Y-axis, 2 = Z-axis
collider: Collider,
}
impl CapsuleCollider {
/// Get the radius of the capsule collider
pub fn radius(&self) -> f32 {
self.radius
}
/// Get the height of the capsule collider
pub fn height(&self) -> f32 {
self.height
}
/// Get the center offset of the capsule collider
pub fn center(&self) -> &Vector3 {
&self.center
}
/// Get the direction axis of the capsule (0=X, 1=Y, 2=Z)
pub fn direction(&self) -> i32 {
self.direction
}
/// Get the base collider component
pub fn collider(&self) -> &Collider {
&self.collider
}
/// Get mutable access to the base collider component
pub fn collider_mut(&mut self) -> &mut Collider {
&mut self.collider
}
/// Set the radius of the capsule collider
pub fn set_radius(&mut self, radius: f32) {
self.radius = radius;
}
/// Set the height of the capsule collider
pub fn set_height(&mut self, height: f32) {
self.height = height;
}
/// Set the center offset of the capsule collider
pub fn set_center(&mut self, center: Vector3) {
self.center = center;
}
/// Set the direction axis (0=X, 1=Y, 2=Z)
pub fn set_direction(&mut self, direction: i32) {
self.direction = direction;
}
}
impl UnityComponent for CapsuleCollider {
/// Parse a CapsuleCollider from YAML
///
/// Note: Caller is responsible for ensuring this is called on the correct document type.
fn parse(yaml: &serde_yaml::Mapping, ctx: &ComponentContext) -> Option<Self> {
let radius = yaml_helpers::get_f64(yaml, "m_Radius").unwrap_or(0.0) as f32;
let height = yaml_helpers::get_f64(yaml, "m_Height").unwrap_or(0.0) as f32;
let center = yaml_helpers::get_vector3(yaml, "m_Center").unwrap_or_default();
let direction = yaml_helpers::get_i64(yaml, "m_Direction").unwrap_or(0) as i32;
// Parse the base collider component (layer masks)
let collider = Collider::parse(yaml, ctx)?;
Some(Self {
radius,
height,
center,
direction,
collider,
})
}
}
impl crate::types::EcsInsertable for CapsuleCollider {
fn insert_into_world(self, world: &mut sparsey::World, entity: Entity) {
// Insert both the CapsuleCollider and the base Collider component
// This allows querying for either specific collider types or all colliders
let collider = self.collider.clone();
world.insert(entity, (self, collider));
}
}
// Register component with inventory
inventory::submit! {
crate::types::ComponentRegistration {
type_id: 136, // Unity type ID for CapsuleCollider
class_name: "CapsuleCollider",
parse_and_insert: |yaml, ctx, world, entity| {
<CapsuleCollider as crate::types::EcsInsertable>::parse_and_insert(yaml, ctx, world, entity)
},
register: |builder| {
// Register both CapsuleCollider and the base Collider component
builder.register::<CapsuleCollider>().register::<Collider>()
},
}
}

View File

@@ -0,0 +1,87 @@
//! Collider component wrapper
use crate::types::{ComponentContext, UnityComponent};
use serde_yaml::{Mapping, Value};
use sparsey::Entity;
/// A Collider component
///
/// Collider is the base class for collision primitives.
/// Contains layer mask information for collision filtering.
#[derive(Debug, Clone)]
pub struct Collider {
/// Layer mask for which layers this collider can collide with
include_layers: u32,
/// Layer mask for which layers this collider should ignore
exclude_layers: u32,
}
impl Collider {
/// Get the include layers bitset
pub fn include_layers(&self) -> u32 {
self.include_layers
}
/// Get the exclude layers bitset
pub fn exclude_layers(&self) -> u32 {
self.exclude_layers
}
/// Set the include layers bitset
pub fn set_include_layers(&mut self, layers: u32) {
self.include_layers = layers;
}
/// Set the exclude layers bitset
pub fn set_exclude_layers(&mut self, layers: u32) {
self.exclude_layers = layers;
}
/// Check if the collider can interact with a specific layer (0-31)
pub fn can_collide_with_layer(&self, layer: u8) -> bool {
if layer >= 32 {
return false;
}
let layer_bit = 1u32 << layer;
(self.include_layers & layer_bit) != 0 && (self.exclude_layers & layer_bit) == 0
}
}
/// Helper function to parse Unity's layer bitset structure
///
/// Unity stores layer masks as:
/// ```yaml
/// m_IncludeLayers:
/// serializedVersion: 2
/// m_Bits: 0
/// ```
fn parse_layer_bitset(map: &Mapping, key: &str) -> Option<u32> {
let obj = map
.get(&Value::String(key.to_string()))
.and_then(|v| v.as_mapping())?;
obj.get(&Value::String("m_Bits".to_string()))
.and_then(|v| v.as_u64())
.map(|bits| bits as u32)
}
impl UnityComponent for Collider {
/// Parse a Collider from YAML
///
/// Note: Caller is responsible for ensuring this is called on the correct document type.
fn parse(yaml: &Mapping, _ctx: &ComponentContext) -> Option<Self> {
let include_layers = parse_layer_bitset(yaml, "m_IncludeLayers").unwrap_or(0);
let exclude_layers = parse_layer_bitset(yaml, "m_ExcludeLayers").unwrap_or(0);
Some(Self {
include_layers,
exclude_layers,
})
}
}
impl crate::types::EcsInsertable for Collider {
fn insert_into_world(self, world: &mut sparsey::World, entity: Entity) {
world.insert(entity, (self,));
}
}

View File

@@ -0,0 +1,54 @@
//! MeshFilter component wrapper
use crate::types::{yaml_helpers, ComponentContext, FileRef, UnityComponent};
use sparsey::Entity;
/// A MeshFilter component
///
/// MeshFilter holds a reference to a mesh asset that is used by the MeshRenderer.
/// It defines the geometry that will be rendered for a GameObject.
#[derive(Debug, Clone)]
pub struct MeshFilter {
mesh: Option<FileRef>,
}
impl MeshFilter {
/// Get the mesh reference
pub fn mesh(&self) -> Option<&FileRef> {
self.mesh.as_ref()
}
/// Set the mesh reference
pub fn set_mesh(&mut self, mesh: Option<FileRef>) {
self.mesh = mesh;
}
}
impl UnityComponent for MeshFilter {
/// Parse a MeshFilter from YAML
///
/// Note: Caller is responsible for ensuring this is called on the correct document type.
fn parse(yaml: &serde_yaml::Mapping, _ctx: &ComponentContext) -> Option<Self> {
let mesh = yaml_helpers::get_file_ref(yaml, "m_Mesh");
Some(Self { mesh })
}
}
impl crate::types::EcsInsertable for MeshFilter {
fn insert_into_world(self, world: &mut sparsey::World, entity: Entity) {
world.insert(entity, (self,));
}
}
// Register component with inventory
inventory::submit! {
crate::types::ComponentRegistration {
type_id: 33,
class_name: "MeshFilter",
parse_and_insert: |yaml, ctx, world, entity| {
<MeshFilter as crate::types::EcsInsertable>::parse_and_insert(yaml, ctx, world, entity)
},
register: |builder| builder.register::<MeshFilter>(),
}
}

View File

@@ -0,0 +1,55 @@
//! MeshRenderer component wrapper
use crate::types::{ComponentContext, Renderer, UnityComponent};
use sparsey::Entity;
/// A MeshRenderer component
///
/// MeshRenderer renders a mesh with materials. It extends the base Renderer
/// with mesh-specific rendering functionality.
#[derive(Debug, Clone)]
pub struct MeshRenderer {
renderer: Renderer,
}
impl MeshRenderer {
/// Get the base Renderer
pub fn renderer(&self) -> &Renderer {
&self.renderer
}
/// Get mutable access to the base Renderer
pub fn renderer_mut(&mut self) -> &mut Renderer {
&mut self.renderer
}
}
impl UnityComponent for MeshRenderer {
/// Parse a MeshRenderer from YAML
///
/// Note: Caller is responsible for ensuring this is called on the correct document type.
fn parse(yaml: &serde_yaml::Mapping, ctx: &ComponentContext) -> Option<Self> {
// Parse the base Renderer data
let renderer = Renderer::parse(yaml, ctx)?;
Some(Self { renderer })
}
}
impl crate::types::EcsInsertable for MeshRenderer {
fn insert_into_world(self, world: &mut sparsey::World, entity: Entity) {
world.insert(entity, (self,));
}
}
// Register component with inventory
inventory::submit! {
crate::types::ComponentRegistration {
type_id: 23,
class_name: "MeshRenderer",
parse_and_insert: |yaml, ctx, world, entity| {
<MeshRenderer as crate::types::EcsInsertable>::parse_and_insert(yaml, ctx, world, entity)
},
register: |builder| builder.register::<MeshRenderer>(),
}
}

View File

@@ -1,11 +1,27 @@
//! Unity-specific types (GameObjects, Transforms, PrefabInstances)
pub mod box_collider;
pub mod capsule_collider;
pub mod collider;
pub mod game_object;
pub mod mesh_filter;
pub mod mesh_renderer;
pub mod prefab_instance;
pub mod renderer;
pub mod sphere_collider;
pub mod stripped_reference;
pub mod transform;
pub use box_collider::BoxCollider;
pub use capsule_collider::CapsuleCollider;
pub use collider::Collider;
pub use game_object::GameObject;
pub use mesh_filter::MeshFilter;
pub use mesh_renderer::MeshRenderer;
pub use prefab_instance::{
PrefabInstance, PrefabInstanceComponent, PrefabModification, PrefabResolver,
};
pub use renderer::Renderer;
pub use sphere_collider::SphereCollider;
pub use stripped_reference::StrippedReference;
pub use transform::{RectTransform, Transform};

View File

@@ -8,6 +8,7 @@ use sparsey::{Entity, World};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::cell::Cell;
/// An instance of a Unity prefab ready for spawning into a scene
///
@@ -20,7 +21,7 @@ use std::sync::Arc;
/// # Example
/// ```ignore
/// let prefab = /* load UnityPrefab */;
/// let mut instance = prefab.instantiate();
/// let mut instance = prefab.instantiate(None);
/// instance.override_value(file_id, "m_Name", "Player1".into())?;
/// instance.override_value(file_id, "m_LocalPosition.x", 100.0.into())?;
/// let entities = instance.spawn_into(&mut world, &mut entity_map)?;
@@ -40,7 +41,8 @@ pub struct PrefabInstance {
/// Sequential counter for generating new FileIDs
/// Starts at i64::MAX and decrements to avoid collisions with scene FileIDs
next_file_id: i64,
/// NOTE: This should be shared across all instances via a reference
next_file_id: Arc<std::cell::Cell<i64>>,
/// Source prefab path for debugging
source_path: PathBuf,
@@ -50,15 +52,22 @@ impl PrefabInstance {
/// Create a new instance from a Unity prefab
///
/// This clones all documents from the prefab and initializes FileID remapping.
pub fn new(prefab: &UnityPrefab) -> Self {
///
/// # Arguments
/// * `prefab` - The prefab to instantiate
/// * `file_id_counter` - Optional shared FileID counter to ensure uniqueness across instances
pub fn new(prefab: &UnityPrefab, file_id_counter: Option<Arc<Cell<i64>>>) -> Self {
// Clone all documents from the prefab
let documents = prefab.documents.clone();
// Use provided counter or create a new one starting at i64::MAX
let next_file_id = file_id_counter.unwrap_or_else(|| Arc::new(Cell::new(i64::MAX)));
let mut instance = Self {
documents,
file_id_map: HashMap::new(),
overrides: HashMap::new(),
next_file_id: i64::MAX,
next_file_id,
source_path: prefab.path.clone(),
};
@@ -73,9 +82,9 @@ impl PrefabInstance {
///
/// Uses a sequential counter starting from i64::MAX and decrementing.
/// This avoids collisions with typical scene FileIDs which are positive.
fn generate_file_id(&mut self) -> FileID {
let id = self.next_file_id;
self.next_file_id -= 1;
fn generate_file_id(&self) -> FileID {
let id = self.next_file_id.get();
self.next_file_id.set(id - 1);
FileID::from_i64(id)
}
@@ -449,6 +458,9 @@ pub struct PrefabResolver<'a> {
/// Prefab GUID resolver for nested prefabs
prefab_guid_resolver: Option<&'a crate::parser::PrefabGuidResolver>,
/// Shared FileID counter to ensure uniqueness across all prefab instances
file_id_counter: Arc<Cell<i64>>,
}
impl<'a> PrefabResolver<'a> {
@@ -463,6 +475,7 @@ impl<'a> PrefabResolver<'a> {
instantiation_stack: Vec::new(),
guid_resolver: None,
prefab_guid_resolver: None,
file_id_counter: Arc::new(Cell::new(i64::MAX)),
}
}
@@ -490,6 +503,7 @@ impl<'a> PrefabResolver<'a> {
instantiation_stack: Vec::new(),
guid_resolver,
prefab_guid_resolver: Some(prefab_guid_resolver),
file_id_counter: Arc::new(Cell::new(i64::MAX)),
}
}
@@ -524,8 +538,8 @@ impl<'a> PrefabResolver<'a> {
// 2. Load prefab via load_prefab()
let prefab = self.load_prefab(guid)?;
// 3. Create PrefabInstance
let mut instance = prefab.instantiate();
// 3. Create PrefabInstance with shared FileID counter
let mut instance = prefab.instantiate(Some(self.file_id_counter.clone()));
// 4. Apply component.modifications using override_value()
for modification in &component.modifications {
@@ -605,8 +619,8 @@ impl<'a> PrefabResolver<'a> {
// Push to stack
self.instantiation_stack.push(prefab_id.clone());
// Create instance
let instance = prefab.instantiate();
// Create instance with shared FileID counter
let instance = prefab.instantiate(Some(self.file_id_counter.clone()));
// Find nested prefab references
let nested_prefabs = self.find_nested_prefabs(&instance)?;
@@ -617,8 +631,8 @@ impl<'a> PrefabResolver<'a> {
for (_parent_file_id, nested_component) in nested_prefabs {
// Load the referenced prefab
if let Ok(nested_prefab) = self.load_prefab(&nested_component.prefab_ref.guid) {
// Apply modifications
let mut nested_instance = nested_prefab.instantiate();
// Apply modifications with shared FileID counter
let mut nested_instance = nested_prefab.instantiate(Some(self.file_id_counter.clone()));
for modification in &nested_component.modifications {
nested_instance.override_value(
modification.target_file_id,

View File

@@ -0,0 +1,56 @@
//! Renderer component wrapper
use crate::types::{yaml_helpers, ComponentContext, FileRef, UnityComponent};
use smallvec::SmallVec;
use sparsey::Entity;
/// A Renderer component
///
/// Renderer is the base class for all rendering components (MeshRenderer, SkinnedMeshRenderer, etc.).
/// It holds references to materials that define how the mesh should be rendered.
#[derive(Debug, Clone)]
pub struct Renderer {
materials: SmallVec<[FileRef; 1]>,
}
impl Renderer {
/// Get the materials references
pub fn materials(&self) -> &[FileRef] {
&self.materials
}
/// Get mutable access to the materials references
pub fn materials_mut(&mut self) -> &mut SmallVec<[FileRef; 1]> {
&mut self.materials
}
/// Set the materials references
pub fn set_materials(&mut self, materials: SmallVec<[FileRef; 1]>) {
self.materials = materials;
}
/// Add a material reference
pub fn add_material(&mut self, material: FileRef) {
self.materials.push(material);
}
}
impl UnityComponent for Renderer {
/// Parse a Renderer from YAML
///
/// Note: Caller is responsible for ensuring this is called on the correct document type.
fn parse(yaml: &serde_yaml::Mapping, _ctx: &ComponentContext) -> Option<Self> {
let materials = yaml_helpers::get_file_ref_array(yaml, "m_Materials")
.unwrap_or_default()
.into_iter()
.collect();
Some(Self { materials })
}
}
impl crate::types::EcsInsertable for Renderer {
fn insert_into_world(self, world: &mut sparsey::World, entity: Entity) {
world.insert(entity, (self,));
}
}

View File

@@ -0,0 +1,91 @@
//! SphereCollider component wrapper
use crate::types::{yaml_helpers, ComponentContext, UnityComponent, Vector3};
use crate::types::unity_types::Collider;
use sparsey::Entity;
/// A SphereCollider component
///
/// SphereCollider is a basic collision primitive in the shape of a sphere.
/// It defines a sphere-shaped volume that can be used for collision detection.
#[derive(Debug, Clone)]
pub struct SphereCollider {
radius: f32,
center: Vector3,
collider: Collider,
}
impl SphereCollider {
/// Get the radius of the sphere collider
pub fn radius(&self) -> f32 {
self.radius
}
/// Get the center offset of the sphere collider
pub fn center(&self) -> &Vector3 {
&self.center
}
/// Get the base collider component
pub fn collider(&self) -> &Collider {
&self.collider
}
/// Get mutable access to the base collider component
pub fn collider_mut(&mut self) -> &mut Collider {
&mut self.collider
}
/// Set the radius of the sphere collider
pub fn set_radius(&mut self, radius: f32) {
self.radius = radius;
}
/// Set the center offset of the sphere collider
pub fn set_center(&mut self, center: Vector3) {
self.center = center;
}
}
impl UnityComponent for SphereCollider {
/// Parse a SphereCollider from YAML
///
/// Note: Caller is responsible for ensuring this is called on the correct document type.
fn parse(yaml: &serde_yaml::Mapping, ctx: &ComponentContext) -> Option<Self> {
let radius = yaml_helpers::get_f64(yaml, "m_Radius").unwrap_or(0.0) as f32;
let center = yaml_helpers::get_vector3(yaml, "m_Center").unwrap_or_default();
// Parse the base collider component (layer masks)
let collider = Collider::parse(yaml, ctx)?;
Some(Self {
radius,
center,
collider,
})
}
}
impl crate::types::EcsInsertable for SphereCollider {
fn insert_into_world(self, world: &mut sparsey::World, entity: Entity) {
// Insert both the SphereCollider and the base Collider component
// This allows querying for either specific collider types or all colliders
let collider = self.collider.clone();
world.insert(entity, (self, collider));
}
}
// Register component with inventory
inventory::submit! {
crate::types::ComponentRegistration {
type_id: 135, // Unity type ID for SphereCollider
class_name: "SphereCollider",
parse_and_insert: |yaml, ctx, world, entity| {
<SphereCollider as crate::types::EcsInsertable>::parse_and_insert(yaml, ctx, world, entity)
},
register: |builder| {
// Register both SphereCollider and the base Collider component
builder.register::<SphereCollider>().register::<Collider>()
},
}
}

View File

@@ -0,0 +1,114 @@
//! Stripped component reference
//!
//! Stripped components are references to components that exist in prefabs
//! but whose data isn't duplicated in the scene file.
use crate::types::{yaml_helpers, ComponentContext, FileID, Guid, UnityComponent};
use sparsey::Entity;
/// A stripped component reference
///
/// Stripped components appear in Unity scenes as lightweight references to
/// prefab components. They have the format:
/// ```yaml
/// --- !u!4 &104494228 stripped
/// Transform:
/// m_CorrespondingSourceObject: {fileID: 1381906096329791709, guid: e959fe449ad88a946b99ba9e7e3617ef, type: 3}
/// m_PrefabInstance: {fileID: 104494225}
/// m_PrefabAsset: {fileID: 0}
/// ```
///
/// The actual component data lives in the prefab file referenced by the GUID.
#[derive(Debug, Clone)]
pub struct StrippedReference {
/// The Unity type name of this component (e.g., "Transform", "GameObject")
pub component_type: String,
/// FileID of the corresponding source object in the prefab
pub source_file_id: Option<FileID>,
/// GUID of the prefab that contains the actual component data
pub prefab_guid: Option<Guid>,
/// FileID of the PrefabInstance this stripped component belongs to
pub prefab_instance: Option<FileID>,
}
impl StrippedReference {
/// Create a new StrippedReference
pub fn new(
component_type: String,
source_file_id: Option<FileID>,
prefab_guid: Option<Guid>,
prefab_instance: Option<FileID>,
) -> Self {
Self {
component_type,
source_file_id,
prefab_guid,
prefab_instance,
}
}
/// Get the component type
pub fn component_type(&self) -> &str {
&self.component_type
}
/// Get the source FileID in the prefab
pub fn source_file_id(&self) -> Option<FileID> {
self.source_file_id
}
/// Get the prefab GUID
pub fn prefab_guid(&self) -> Option<Guid> {
self.prefab_guid
}
/// Get the PrefabInstance FileID
pub fn prefab_instance(&self) -> Option<FileID> {
self.prefab_instance
}
}
impl UnityComponent for StrippedReference {
/// Parse a stripped component reference from YAML
///
/// Extracts the m_CorrespondingSourceObject and m_PrefabInstance references.
fn parse(yaml: &serde_yaml::Mapping, ctx: &ComponentContext) -> Option<Self> {
use serde_yaml::Value;
// Extract m_CorrespondingSourceObject: {fileID: ..., guid: ..., type: 3}
// This reference has both fileID (source object in prefab) and guid (prefab GUID)
let source_obj = yaml
.get(&Value::String("m_CorrespondingSourceObject".to_string()))
.and_then(|v| v.as_mapping());
let source_file_id = source_obj
.and_then(|obj| obj.get(&Value::String("fileID".to_string())))
.and_then(|v| v.as_i64())
.map(FileID::from_i64);
let prefab_guid = source_obj
.and_then(|obj| obj.get(&Value::String("guid".to_string())))
.and_then(|v| v.as_str())
.and_then(|s| Guid::from_hex(s).ok());
// Extract m_PrefabInstance: {fileID: ...}
let prefab_instance_ref = yaml_helpers::get_file_ref(yaml, "m_PrefabInstance");
let prefab_instance = prefab_instance_ref.map(|r| r.file_id);
Some(Self {
component_type: ctx.class_name.to_string(),
source_file_id,
prefab_guid,
prefab_instance,
})
}
}
impl crate::types::EcsInsertable for StrippedReference {
fn insert_into_world(self, world: &mut sparsey::World, entity: Entity) {
world.insert(entity, (self,));
}
}

View File

@@ -1,5 +1,7 @@
//! Transform and RectTransform component wrappers
use log::warn;
use crate::types::{yaml_helpers, ComponentContext, Quaternion, UnityComponent, Vector2, Vector3};
use sparsey::Entity;
@@ -104,10 +106,7 @@ impl UnityComponent for Transform {
if let Some(child_entity) = entity_map.get(child_file_id).copied() {
all_children.push(child_entity);
} else {
eprintln!(
"Warning: Could not resolve child Transform: {}",
child_file_id
);
warn!("Could not resolve child Transform");
}
}
@@ -264,10 +263,7 @@ impl UnityComponent for RectTransform {
if let Some(child_entity) = entity_map.get(child_file_id).copied() {
all_children.push(child_entity);
} else {
eprintln!(
"Warning: Could not resolve child RectTransform: {}",
child_file_id
);
warn!("Could not resolve child RectTransform");
}
}

View File

@@ -1,554 +0,0 @@
//! Integration tests for parsing real Unity projects
use unity_parser::{GuidResolver, UnityFile};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Instant;
/// Test project configuration
struct TestProject {
name: &'static str,
repo_url: &'static str,
branch: Option<&'static str>,
}
impl TestProject {
const VR_HORROR: TestProject = TestProject {
name: "VR_Horror_YouCantRun",
repo_url: "https://github.com/Unity3D-Projects/VR_Horror_YouCantRun.git",
branch: None,
};
const PIRATE_PANIC: TestProject = TestProject {
name: "PiratePanic",
repo_url: "https://github.com/Unity-Technologies/PiratePanic.git",
branch: None,
};
}
/// Statistics gathered during parsing
#[derive(Debug, Default)]
struct ParsingStats {
total_files: usize,
scenes: usize,
prefabs: usize,
assets: usize,
errors: Vec<(PathBuf, String)>,
total_entities: usize,
total_documents: usize,
parse_time_ms: u128,
}
impl ParsingStats {
fn print_summary(&self) {
println!("\n{}", "=".repeat(60));
println!("Parsing Statistics");
println!("{}", "=".repeat(60));
println!(" Total files found: {}", self.total_files);
println!(" Scenes parsed: {}", self.scenes);
println!(" Prefabs parsed: {}", self.prefabs);
println!(" Assets parsed: {}", self.assets);
println!(" Total entities: {}", self.total_entities);
println!(" Total documents: {}", self.total_documents);
println!(" Parse time: {} ms", self.parse_time_ms);
if !self.errors.is_empty() {
println!("\n Errors encountered: {}", self.errors.len());
println!("\n Error details:");
for (path, error) in self.errors.iter().take(10) {
println!(" - {}", path.display());
println!(" Error: {}", error);
}
if self.errors.len() > 10 {
println!(" ... and {} more errors", self.errors.len() - 10);
}
}
let success_rate = if self.total_files > 0 {
((self.total_files - self.errors.len()) as f64 / self.total_files as f64) * 100.0
} else {
0.0
};
println!("\n Success rate: {:.2}%", success_rate);
println!("{}", "=".repeat(60));
}
}
/// Clone a git repository for testing
fn clone_test_project(project: &TestProject) -> std::io::Result<PathBuf> {
let test_data_dir = PathBuf::from("test_data");
std::fs::create_dir_all(&test_data_dir)?;
let project_path = test_data_dir.join(project.name);
// Skip if already cloned
if project_path.exists() {
println!("Project already cloned at: {}", project_path.display());
return Ok(project_path);
}
println!("Cloning {} from {}...", project.name, project.repo_url);
let mut cmd = Command::new("git");
cmd.arg("clone");
if let Some(branch) = project.branch {
cmd.arg("--branch").arg(branch);
}
cmd.arg("--depth").arg("1"); // Shallow clone for speed
cmd.arg(project.repo_url);
cmd.arg(&project_path);
let output = cmd.output()?;
if !output.status.success() {
eprintln!("Git clone failed: {}", String::from_utf8_lossy(&output.stderr));
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Git clone failed",
));
}
println!("Successfully cloned to: {}", project_path.display());
Ok(project_path)
}
/// Recursively find all Unity files in a directory
fn find_unity_files(dir: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
if !dir.exists() || !dir.is_dir() {
return files;
}
fn visit_dir(dir: &Path, files: &mut Vec<PathBuf>) {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
// Skip common Unity directories that don't contain source assets
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if name == "Library" || name == "Temp" || name == "Builds" || name == ".git" {
continue;
}
}
if path.is_dir() {
visit_dir(&path, files);
} else if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
if ext == "unity" || ext == "prefab" || ext == "asset" {
files.push(path);
}
}
}
}
}
visit_dir(dir, &mut files);
files
}
/// Parse all Unity files in a project and collect statistics
fn parse_project(project_path: &Path) -> ParsingStats {
let mut stats = ParsingStats::default();
println!("\nFinding Unity files in {}...", project_path.display());
let files = find_unity_files(project_path);
stats.total_files = files.len();
println!("Found {} Unity files", files.len());
println!("\nParsing files...");
let start_time = Instant::now();
for (i, file_path) in files.iter().enumerate() {
// Print progress
if (i + 1) % 10 == 0 || i == 0 {
println!(
" [{}/{}] Parsing: {}",
i + 1,
files.len(),
file_path.file_name().unwrap().to_string_lossy()
);
}
match UnityFile::from_path(file_path) {
Ok(unity_file) => match unity_file {
UnityFile::Scene(scene) => {
stats.scenes += 1;
stats.total_entities += scene.entity_map.len();
}
UnityFile::Prefab(prefab) => {
stats.prefabs += 1;
stats.total_documents += prefab.documents.len();
}
UnityFile::Asset(asset) => {
stats.assets += 1;
stats.total_documents += asset.documents.len();
}
},
Err(e) => {
stats.errors.push((file_path.clone(), e.to_string()));
}
}
}
stats.parse_time_ms = start_time.elapsed().as_millis();
stats
}
/// Test parsing a specific project
fn test_project(project: &TestProject) {
println!("\n{}", "=".repeat(60));
println!("Testing: {}", project.name);
println!("{}", "=".repeat(60));
// Clone the project
let project_path = match clone_test_project(project) {
Ok(path) => path,
Err(e) => {
eprintln!("Failed to clone project: {}", e);
eprintln!("Skipping project test (git may not be available)");
return;
}
};
// Parse all files
let stats = parse_project(&project_path);
// Print summary
stats.print_summary();
// Assert basic expectations
assert!(
stats.total_files > 0,
"Should find at least some Unity files"
);
// Allow some errors but not too many
let error_rate = if stats.total_files > 0 {
(stats.errors.len() as f64 / stats.total_files as f64) * 100.0
} else {
0.0
};
if error_rate > 50.0 {
panic!(
"Error rate too high: {:.2}% ({}/{})",
error_rate, stats.errors.len(), stats.total_files
);
}
}
/// Test detailed parsing of specific file types
fn test_detailed_parsing(project_path: &Path) {
println!("\n{}", "=".repeat(60));
println!("Detailed Parsing Tests");
println!("{}", "=".repeat(60));
let files = find_unity_files(project_path);
// Test scene parsing
if let Some(scene_file) = files.iter().find(|f| {
f.extension()
.and_then(|e| e.to_str())
.map_or(false, |e| e == "unity")
}) {
println!(
"\nTesting scene parsing: {}",
scene_file.file_name().unwrap().to_string_lossy()
);
match UnityFile::from_path(scene_file) {
Ok(UnityFile::Scene(scene)) => {
println!(" ✓ Successfully parsed scene");
println!(" - Entities: {}", scene.entity_map.len());
println!(" - Path: {}", scene.path.display());
// Try to access entities
for (file_id, entity) in scene.entity_map.iter().take(3) {
println!(" - FileID {} -> Entity {:?}", file_id, entity);
}
}
Ok(_) => println!(" ✗ File was not parsed as scene"),
Err(e) => println!(" ✗ Parse error: {}", e),
}
}
// Test prefab parsing and instancing
if let Some(prefab_file) = files.iter().find(|f| {
f.extension()
.and_then(|e| e.to_str())
.map_or(false, |e| e == "prefab")
}) {
println!(
"\nTesting prefab parsing: {}",
prefab_file.file_name().unwrap().to_string_lossy()
);
match UnityFile::from_path(prefab_file) {
Ok(UnityFile::Prefab(prefab)) => {
println!(" ✓ Successfully parsed prefab");
println!(" - Documents: {}", prefab.documents.len());
println!(" - Path: {}", prefab.path.display());
// Test instantiation
println!("\n Testing prefab instantiation:");
let instance = prefab.instantiate();
println!(
" ✓ Created instance with {} remapped FileIDs",
instance.file_id_map().len()
);
// Test override system
if let Some(first_doc) = prefab.documents.first() {
let mut instance2 = prefab.instantiate();
let result = instance2.override_value(
first_doc.file_id,
"m_Name",
serde_yaml::Value::String("TestName".to_string()),
);
if result.is_ok() {
println!(" ✓ Override system working");
} else {
println!(" - Override test: {}", result.unwrap_err());
}
}
// List document types
let mut type_counts = std::collections::HashMap::new();
for doc in &prefab.documents {
*type_counts.entry(&doc.class_name).or_insert(0) += 1;
}
println!(" - Component types:");
for (class_name, count) in type_counts.iter() {
println!(" - {}: {}", class_name, count);
}
}
Ok(_) => println!(" ✗ File was not parsed as prefab"),
Err(e) => println!(" ✗ Parse error: {}", e),
}
}
}
#[test]
fn test_vr_horror_project() {
test_project(&TestProject::VR_HORROR);
}
#[test]
#[ignore] // Ignore by default, run with --ignored to test
fn test_pirate_panic_project() {
test_project(&TestProject::PIRATE_PANIC);
}
#[test]
fn test_vr_horror_detailed() {
let project_path = match clone_test_project(&TestProject::VR_HORROR) {
Ok(path) => path,
Err(e) => {
eprintln!("Failed to clone project: {}", e);
eprintln!("Skipping detailed test (git may not be available)");
return;
}
};
test_detailed_parsing(&project_path);
}
/// Benchmark parsing performance
#[test]
#[ignore]
fn benchmark_parsing() {
let project_path = match clone_test_project(&TestProject::VR_HORROR) {
Ok(path) => path,
Err(_) => {
eprintln!("Skipping benchmark (git not available)");
return;
}
};
println!("\n{}", "=".repeat(60));
println!("Parsing Performance Benchmark");
println!("{}", "=".repeat(60));
let files = find_unity_files(&project_path);
let total_size: u64 = files
.iter()
.filter_map(|f| std::fs::metadata(f).ok())
.map(|m| m.len())
.sum();
println!("Total files: {}", files.len());
println!("Total size: {} KB", total_size / 1024);
let start = Instant::now();
let stats = parse_project(&project_path);
let elapsed = start.elapsed();
println!("\nParsing completed in {:?}", elapsed);
println!(
"Average time per file: {:.2} ms",
elapsed.as_millis() as f64 / files.len() as f64
);
println!(
"Throughput: {:.2} files/sec",
files.len() as f64 / elapsed.as_secs_f64()
);
println!(
"Throughput: {:.2} KB/sec",
(total_size / 1024) as f64 / elapsed.as_secs_f64()
);
stats.print_summary();
}
/// Test GUID resolution for MonoBehaviour scripts
#[test]
fn test_guid_resolution() {
let project_path = match clone_test_project(&TestProject::VR_HORROR) {
Ok(path) => path,
Err(e) => {
eprintln!("Failed to clone project: {}", e);
eprintln!("Skipping GUID resolution test (git may not be available)");
return;
}
};
println!("\n{}", "=".repeat(60));
println!("Testing GUID Resolution");
println!("{}", "=".repeat(60));
// Build GUID resolver from project
println!("\nBuilding GuidResolver from project...");
let resolver = GuidResolver::from_project(&project_path)
.expect("Should successfully build GuidResolver");
println!(" ✓ Found {} GUID mappings", resolver.len());
assert!(
!resolver.is_empty(),
"Should find at least some C# scripts with GUIDs"
);
// Test the known PlaySFX GUID from the roadmap
let playsfx_guid_str = "091c537484687e9419460cdcd7038234";
println!("\nTesting known GUID resolution:");
println!(" GUID: {}", playsfx_guid_str);
// Test resolution by string
match resolver.resolve_class_name(playsfx_guid_str) {
Some(class_name) => {
println!(" ✓ Resolved by string to: {}", class_name);
assert_eq!(
class_name, "PlaySFX",
"PlaySFX GUID should resolve to 'PlaySFX' class name"
);
}
None => {
panic!("Failed to resolve PlaySFX GUID. Available GUIDs: {:?}",
resolver.guids().take(5).collect::<Vec<_>>());
}
}
// Test resolution by Guid type
use unity_parser::Guid;
let playsfx_guid = Guid::from_hex(playsfx_guid_str).unwrap();
match resolver.resolve_class_name(&playsfx_guid) {
Some(class_name) => {
println!(" ✓ Resolved by Guid type to: {}", class_name);
assert_eq!(class_name, "PlaySFX");
}
None => panic!("Failed to resolve PlaySFX GUID by Guid type"),
}
// Show sample of resolved GUIDs
println!("\nSample of resolved GUIDs:");
for guid in resolver.guids().take(5) {
if let Some(class_name) = resolver.resolve_class_name(&guid) {
println!(" {}{}", guid, class_name);
}
}
// Demonstrate memory efficiency
println!("\nMemory efficiency:");
println!(" Each Guid: {} bytes (u128)", std::mem::size_of::<Guid>());
println!(" Each String GUID: ~{} bytes (heap allocated)", 32 + std::mem::size_of::<String>());
println!("\n{}", "=".repeat(60));
}
/// Test parsing PlaySFX components from actual scene file
#[test]
fn test_playsfx_parsing() {
use unity_parser::UnityComponent;
/// PlaySFX component from VR_Horror_YouCantRun
#[derive(Debug, Clone, UnityComponent)]
#[unity_class("PlaySFX")]
pub struct PlaySFX {
#[unity_field("volume")]
pub volume: f64,
#[unity_field("startTime")]
pub start_time: f64,
#[unity_field("endTime")]
pub end_time: f64,
#[unity_field("isLoop")]
pub is_loop: bool,
}
let project_path = match clone_test_project(&TestProject::VR_HORROR) {
Ok(path) => path,
Err(e) => {
eprintln!("Failed to clone project: {}", e);
eprintln!("Skipping PlaySFX parsing test (git may not be available)");
return;
}
};
println!("\n{}", "=".repeat(60));
println!("Testing PlaySFX Component Parsing");
println!("{}", "=".repeat(60));
// Parse the 1F.unity scene that contains PlaySFX components
let scene_path = project_path.join("Assets/Scenes/TEST/Final_1F/1F.unity");
if !scene_path.exists() {
eprintln!("Scene file not found: {}", scene_path.display());
return;
}
println!("\n Parsing scene: {}", scene_path.display());
match unity_parser::UnityFile::from_path(&scene_path) {
Ok(unity_parser::UnityFile::Scene(scene)) => {
println!(" ✓ Scene parsed successfully");
println!(" - Total entities: {}", scene.entity_map.len());
// Try to get PlaySFX components
let playsfx_view = scene.world.borrow::<PlaySFX>();
let mut found_count = 0;
for entity in scene.entity_map.values() {
if playsfx_view.get(*entity).is_some() {
found_count += 1;
}
}
println!(" ✓ Found {} PlaySFX component(s)", found_count);
assert!(
found_count > 0,
"Should find at least one PlaySFX component in 1F.unity"
);
}
Ok(_) => {
panic!("File was not parsed as a scene");
}
Err(e) => {
panic!("Failed to parse scene: {}", e);
}
}
println!("\n{}", "=".repeat(60));
}

View File

@@ -1,197 +0,0 @@
//! Tests for the #[derive(UnityComponent)] macro
use unity_parser::{ComponentContext, FileID, UnityComponent};
/// Test component matching the PlaySFX script from VR_Horror_YouCantRun
#[derive(Debug, Clone, UnityComponent)]
#[unity_class("PlaySFX")]
struct PlaySFX {
#[unity_field("volume")]
volume: f64,
#[unity_field("startTime")]
start_time: f64,
#[unity_field("endTime")]
end_time: f64,
#[unity_field("isLoop")]
is_loop: bool,
}
/// Test component with different field types
#[derive(Debug, Clone, UnityComponent)]
#[unity_class("TestComponent")]
struct TestComponent {
#[unity_field("floatValue")]
float_value: f32,
#[unity_field("intValue")]
int_value: i32,
#[unity_field("stringValue")]
string_value: String,
#[unity_field("boolValue")]
bool_value: bool,
}
#[test]
fn test_play_sfx_parsing() {
let yaml_str = r#"
volume: 0.75
startTime: 1.5
endTime: 3.0
isLoop: 1
"#;
let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap();
let mapping = yaml.as_mapping().unwrap();
let ctx = ComponentContext {
type_id: 114,
file_id: FileID::from_i64(12345),
class_name: "PlaySFX",
entity: None,
linking_ctx: None,
yaml: mapping,
};
let result = PlaySFX::parse(mapping, &ctx);
assert!(result.is_some(), "Failed to parse PlaySFX component");
let component = result.unwrap();
assert_eq!(component.volume, 0.75);
assert_eq!(component.start_time, 1.5);
assert_eq!(component.end_time, 3.0);
assert_eq!(component.is_loop, true);
}
#[test]
fn test_play_sfx_default_values() {
// Test with missing fields (should use Default::default())
let yaml_str = r#"
volume: 0.5
"#;
let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap();
let mapping = yaml.as_mapping().unwrap();
let ctx = ComponentContext {
type_id: 114,
file_id: FileID::from_i64(12345),
class_name: "PlaySFX",
entity: None,
linking_ctx: None,
yaml: mapping,
};
let result = PlaySFX::parse(mapping, &ctx);
assert!(result.is_some(), "Failed to parse PlaySFX component with defaults");
let component = result.unwrap();
assert_eq!(component.volume, 0.5);
assert_eq!(component.start_time, 0.0); // Default for f64
assert_eq!(component.end_time, 0.0); // Default for f64
assert_eq!(component.is_loop, false); // Default for bool
}
#[test]
fn test_test_component_parsing() {
let yaml_str = r#"
floatValue: 3.14
intValue: 42
stringValue: "Hello, Unity!"
boolValue: 1
"#;
let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap();
let mapping = yaml.as_mapping().unwrap();
let ctx = ComponentContext {
type_id: 114,
file_id: FileID::from_i64(67890),
class_name: "TestComponent",
entity: None,
linking_ctx: None,
yaml: mapping,
};
let result = TestComponent::parse(mapping, &ctx);
assert!(result.is_some(), "Failed to parse TestComponent");
let component = result.unwrap();
assert!((component.float_value - 3.14_f32).abs() < 0.001);
assert_eq!(component.int_value, 42);
assert_eq!(component.string_value, "Hello, Unity!");
assert_eq!(component.bool_value, true);
}
#[test]
fn test_component_registration() {
// Verify that components are registered in the inventory
let mut found_play_sfx = false;
let mut found_test_component = false;
for reg in inventory::iter::<unity_parser::ComponentRegistration> {
if reg.class_name == "PlaySFX" {
found_play_sfx = true;
assert_eq!(reg.type_id, 114);
}
if reg.class_name == "TestComponent" {
found_test_component = true;
assert_eq!(reg.type_id, 114);
}
}
assert!(
found_play_sfx,
"PlaySFX component was not registered in inventory"
);
assert!(
found_test_component,
"TestComponent was not registered in inventory"
);
}
#[test]
fn test_component_registration_parser() {
// Test that the registered parser function works
let yaml_str = r#"
volume: 0.8
startTime: 2.0
endTime: 4.0
isLoop: 0
"#;
let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap();
let mapping = yaml.as_mapping().unwrap();
let ctx = ComponentContext {
type_id: 114,
file_id: FileID::from_i64(11111),
class_name: "PlaySFX",
entity: None,
linking_ctx: None,
yaml: mapping,
};
// Find the PlaySFX registration and call its parser
for reg in inventory::iter::<unity_parser::ComponentRegistration> {
if reg.class_name == "PlaySFX" {
let result = (reg.parser)(mapping, &ctx);
assert!(result.is_some(), "Registered parser failed to parse");
// Downcast to verify it's the right type
let boxed = result.unwrap();
assert!(
boxed.downcast_ref::<PlaySFX>().is_some(),
"Parsed component is not PlaySFX type"
);
return;
}
}
panic!("PlaySFX registration not found");
}