project load first
This commit is contained in:
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
|
||||||
@@ -9,12 +9,12 @@
|
|||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
use types::InteractableResource;
|
use types::InteractableResource;
|
||||||
use unity_parser::UnityFile;
|
use unity_parser::UnityProject;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use unity_parser::log::DedupLogger;
|
use unity_parser::log::DedupLogger;
|
||||||
use log::{info, warn, error, LevelFilter};
|
use log::{info, warn, error, LevelFilter};
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
||||||
@@ -22,23 +22,23 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
log::set_boxed_logger(Box::new(logger))
|
log::set_boxed_logger(Box::new(logger))
|
||||||
.map(|()| log::set_max_level(LevelFilter::Trace))
|
.map(|()| log::set_max_level(LevelFilter::Trace))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
log::set_max_level(LevelFilter::Warn);
|
// log::set_max_level(LevelFilter::Warn);
|
||||||
|
|
||||||
info!("🎮 Cursebreaker - Resource Parser");
|
info!("🎮 Cursebreaker - Resource Parser");
|
||||||
|
|
||||||
let scene_path = Path::new("/home/connor/repos/CBAssets/_GameAssets/Scenes/Tiles/10_3.unity");
|
// 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());
|
||||||
|
|
||||||
// Check if scene exists
|
let project = UnityProject::from_path(project_root)?;
|
||||||
if !scene_path.exists() {
|
|
||||||
error!("Scene not found at {}", scene_path.display());
|
|
||||||
return Err("Scene file not found".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("📁 Parsing scene: {}", scene_path.display());
|
// Now parse the scene using the pre-built GUID resolvers
|
||||||
|
let scene_path = "_GameAssets/Scenes/Tiles/10_3.unity";
|
||||||
|
info!("📁 Parsing scene: {}", scene_path);
|
||||||
|
|
||||||
// Parse the scene
|
// Parse the scene using the project
|
||||||
match UnityFile::from_path(&scene_path) {
|
match project.parse_scene(scene_path) {
|
||||||
Ok(UnityFile::Scene(scene)) => {
|
Ok(scene) => {
|
||||||
info!("✅ Scene parsed successfully!");
|
info!("✅ Scene parsed successfully!");
|
||||||
info!(" Total entities: {}", scene.entity_map.len());
|
info!(" Total entities: {}", scene.entity_map.len());
|
||||||
|
|
||||||
@@ -114,10 +114,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
info!("✅ Parsing complete!");
|
info!("✅ Parsing complete!");
|
||||||
}
|
}
|
||||||
Ok(_) => {
|
|
||||||
error!("File is not a scene");
|
|
||||||
return Err("Not a Unity scene file".into());
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Parse error: {}", e);
|
error!("Parse error: {}", e);
|
||||||
return Err(Box::new(e));
|
return Err(Box::new(e));
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
Cursebreaker Resources - 10_3.unity Scene
|
Cursebreaker Resources - 10_3.unity Scene
|
||||||
======================================================================
|
======================================================================
|
||||||
|
|
||||||
Total resources found: 2
|
Total resources found: 1
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
Resource: HarvestableSpawner_11Redberries
|
|
||||||
TypeID: 11
|
|
||||||
MaxHealth: 0
|
|
||||||
Position: (1769.135864, 32.664658, 150.395081)
|
|
||||||
|
|
||||||
Resource: HarvestableSpawner_38Dandelions
|
Resource: HarvestableSpawner_38Dandelions
|
||||||
TypeID: 38
|
TypeID: 38
|
||||||
MaxHealth: 0
|
MaxHealth: 0
|
||||||
|
|||||||
@@ -38,13 +38,11 @@ pub mod types;
|
|||||||
|
|
||||||
// Re-exports
|
// Re-exports
|
||||||
pub use error::{Error, Result};
|
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::{
|
pub use parser::{
|
||||||
find_project_root, meta::MetaFile, parse_unity_file, parse_unity_file_filtered,
|
find_project_root, meta::MetaFile, parse_unity_file, parse_unity_file_filtered,
|
||||||
GuidResolver, PrefabGuidResolver,
|
GuidResolver, PrefabGuidResolver,
|
||||||
};
|
};
|
||||||
// TODO: Re-enable once project module is updated
|
|
||||||
// pub use project::UnityProject;
|
|
||||||
pub use property::PropertyValue;
|
pub use property::PropertyValue;
|
||||||
pub use types::{
|
pub use types::{
|
||||||
get_class_name, get_type_id, Color, ComponentContext, ComponentRegistration, EcsInsertable,
|
get_class_name, get_type_id, Color, ComponentContext, ComponentRegistration, EcsInsertable,
|
||||||
|
|||||||
@@ -4,10 +4,12 @@
|
|||||||
//! Unity files can be Scenes (.unity), Prefabs (.prefab), or Assets (.asset), each
|
//! Unity files can be Scenes (.unity), Prefabs (.prefab), or Assets (.asset), each
|
||||||
//! with different handling requirements.
|
//! with different handling requirements.
|
||||||
|
|
||||||
|
use crate::parser::{GuidResolver, PrefabGuidResolver};
|
||||||
use crate::types::FileID;
|
use crate::types::FileID;
|
||||||
|
use log::info;
|
||||||
use sparsey::{Entity, World};
|
use sparsey::{Entity, World};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
/// A parsed Unity file - can be a Scene, Prefab, or Asset
|
/// A parsed Unity file - can be a Scene, Prefab, or Asset
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -210,3 +212,132 @@ impl RawDocument {
|
|||||||
self.yaml.as_mapping()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ impl GuidResolver {
|
|||||||
/// parses them to extract GUIDs, then parses the corresponding
|
/// parses them to extract GUIDs, then parses the corresponding
|
||||||
/// C# files to extract class names.
|
/// 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
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `project_path` - Path to the Unity project root (containing Assets/ folder)
|
/// * `project_path` - Path to the Unity project root (containing Assets/ folder)
|
||||||
@@ -94,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();
|
let mut resolver = Self::new();
|
||||||
|
|
||||||
// Walk the Assets directory looking for .cs.meta files
|
// Walk the directory looking for .cs.meta files
|
||||||
for entry in WalkDir::new(&assets_dir)
|
for entry in WalkDir::new(dir)
|
||||||
.follow_links(false)
|
.follow_links(false)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|e| e.ok())
|
.filter_map(|e| e.ok())
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ pub use yaml::split_yaml_documents;
|
|||||||
|
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
|
|
||||||
use crate::model::{RawDocument, UnityAsset, UnityFile, UnityPrefab, UnityScene};
|
use crate::model::{RawDocument, UnityAsset, UnityFile, UnityPrefab, UnityScene, UnityProject};
|
||||||
use crate::types::{FileID, Guid, TypeFilter};
|
use crate::types::{FileID, Guid, TypeFilter};
|
||||||
use crate::{Error, Result};
|
use crate::{Error, Result};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
@@ -198,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
|
/// Parse a prefab file into raw YAML documents
|
||||||
fn parse_prefab(path: &Path, content: &str, type_filter: Option<&TypeFilter>) -> Result<UnityFile> {
|
fn parse_prefab(path: &Path, content: &str, type_filter: Option<&TypeFilter>) -> Result<UnityFile> {
|
||||||
let raw_documents = parse_raw_documents(content, type_filter)?;
|
let raw_documents = parse_raw_documents(content, type_filter)?;
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ impl PrefabGuidResolver {
|
|||||||
/// This scans for all `.prefab.meta` files and extracts their GUIDs
|
/// This scans for all `.prefab.meta` files and extracts their GUIDs
|
||||||
/// to build a GUID → Prefab Path mapping.
|
/// 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
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `project_path` - Path to the Unity project root (containing Assets/ folder)
|
/// * `project_path` - Path to the Unity project root (containing Assets/ folder)
|
||||||
@@ -90,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();
|
let mut resolver = Self::new();
|
||||||
|
|
||||||
// Walk the Assets directory looking for .prefab.meta files
|
// Walk the directory looking for .prefab.meta files
|
||||||
for entry in WalkDir::new(&assets_dir)
|
for entry in WalkDir::new(dir)
|
||||||
.follow_links(false)
|
.follow_links(false)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|e| e.ok())
|
.filter_map(|e| e.ok())
|
||||||
|
|||||||
Reference in New Issue
Block a user