project load first

This commit is contained in:
2026-01-05 08:12:55 +00:00
parent c58488c5fa
commit 1867c5559b
8 changed files with 940 additions and 32 deletions

627
PREFAB_SUMMARY.md Normal file
View File

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

View File

@@ -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));

View File

@@ -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

View File

@@ -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,

View File

@@ -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)
}
}
}

View File

@@ -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())

View File

@@ -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)?;

View File

@@ -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())