Compare commits
12 Commits
8a06185b98
...
0f2cb68fa4
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f2cb68fa4 | |||
| dea28f5b9c | |||
| 1867c5559b | |||
| c58488c5fa | |||
| 71a031c3f9 | |||
| 16e83aca67 | |||
| b3f09bb742 | |||
| 31662cda3b | |||
| 37d6a9e6fc | |||
| 708cf77df1 | |||
| c570e2b11a | |||
| 29813a46ef |
@@ -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
25
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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
627
PREFAB_SUMMARY.md
Normal 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
|
||||
81
README.md
81
README.md
@@ -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
|
||||
|
||||
11
cursebreaker-parser/Cargo.toml
Normal file
11
cursebreaker-parser/Cargo.toml
Normal 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"] }
|
||||
@@ -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(())
|
||||
}
|
||||
47
cursebreaker-parser/src/types/interactable_resource.rs
Normal file
47
cursebreaker-parser/src/types/interactable_resource.rs
Normal 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>(),
|
||||
}
|
||||
}
|
||||
3
cursebreaker-parser/src/types/mod.rs
Normal file
3
cursebreaker-parser/src/types/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod interactable_resource;
|
||||
|
||||
pub use interactable_resource::InteractableResource;
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!");
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
.borrow()
|
||||
.entity_map()
|
||||
.get(&r.file_id)
|
||||
.copied()
|
||||
.ok_or_else(|| {
|
||||
Error::reference_error(format!("Unknown GameObject: {}", r.file_id))
|
||||
})?,
|
||||
Some(r) => {
|
||||
let entity_opt = linking_ctx
|
||||
.borrow()
|
||||
.entity_map()
|
||||
.get(&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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
107
unity-parser/src/log/dedup_logger.rs
Normal file
107
unity-parser/src/log/dedup_logger.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
3
unity-parser/src/log/mod.rs
Normal file
3
unity-parser/src/log/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod dedup_logger;
|
||||
|
||||
pub use dedup_logger::DedupLogger;
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>(())
|
||||
/// ```
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
)))
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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};
|
||||
|
||||
87
unity-parser/src/types/unity_types/box_collider.rs
Normal file
87
unity-parser/src/types/unity_types/box_collider.rs
Normal 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>()
|
||||
},
|
||||
}
|
||||
}
|
||||
117
unity-parser/src/types/unity_types/capsule_collider.rs
Normal file
117
unity-parser/src/types/unity_types/capsule_collider.rs
Normal 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>()
|
||||
},
|
||||
}
|
||||
}
|
||||
87
unity-parser/src/types/unity_types/collider.rs
Normal file
87
unity-parser/src/types/unity_types/collider.rs
Normal 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,));
|
||||
}
|
||||
}
|
||||
54
unity-parser/src/types/unity_types/mesh_filter.rs
Normal file
54
unity-parser/src/types/unity_types/mesh_filter.rs
Normal 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>(),
|
||||
}
|
||||
}
|
||||
55
unity-parser/src/types/unity_types/mesh_renderer.rs
Normal file
55
unity-parser/src/types/unity_types/mesh_renderer.rs
Normal 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>(),
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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,
|
||||
|
||||
56
unity-parser/src/types/unity_types/renderer.rs
Normal file
56
unity-parser/src/types/unity_types/renderer.rs
Normal 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,));
|
||||
}
|
||||
}
|
||||
91
unity-parser/src/types/unity_types/sphere_collider.rs
Normal file
91
unity-parser/src/types/unity_types/sphere_collider.rs
Normal 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>()
|
||||
},
|
||||
}
|
||||
}
|
||||
114
unity-parser/src/types/unity_types/stripped_reference.rs
Normal file
114
unity-parser/src/types/unity_types/stripped_reference.rs
Normal 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,));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user