From 1867c5559b71cbf14ca2c83b0961a7d9e595752b Mon Sep 17 00:00:00 2001 From: Connor Date: Mon, 5 Jan 2026 08:12:55 +0000 Subject: [PATCH] project load first --- PREFAB_SUMMARY.md | 627 ++++++++++++++++++ cursebreaker-parser/src/main.rs | 30 +- resources_output.txt | 7 +- unity-parser/src/lib.rs | 4 +- unity-parser/src/model/mod.rs | 133 +++- unity-parser/src/parser/guid_resolver.rs | 46 +- unity-parser/src/parser/mod.rs | 79 ++- .../src/parser/prefab_guid_resolver.rs | 46 +- 8 files changed, 940 insertions(+), 32 deletions(-) create mode 100644 PREFAB_SUMMARY.md diff --git a/PREFAB_SUMMARY.md b/PREFAB_SUMMARY.md new file mode 100644 index 0000000..5761b54 --- /dev/null +++ b/PREFAB_SUMMARY.md @@ -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, +} +``` + +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, +} + +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>, + + /// Mapping from GUID to file path + guid_to_path: HashMap, + + /// Stack for circular reference detection + instantiation_stack: Vec, + + /// 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 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> { + // 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> { + // 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::(); + let transforms = scene.world.borrow::(); + + 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> { + // 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 diff --git a/cursebreaker-parser/src/main.rs b/cursebreaker-parser/src/main.rs index bb87495..6afc4b8 100644 --- a/cursebreaker-parser/src/main.rs +++ b/cursebreaker-parser/src/main.rs @@ -9,12 +9,12 @@ mod types; use types::InteractableResource; -use unity_parser::UnityFile; +use unity_parser::UnityProject; use std::fs::File; use std::io::Write; use std::path::Path; use unity_parser::log::DedupLogger; - use log::{info, warn, error, LevelFilter}; +use log::{info, warn, error, LevelFilter}; fn main() -> Result<(), Box> { @@ -22,23 +22,23 @@ fn main() -> Result<(), Box> { log::set_boxed_logger(Box::new(logger)) .map(|()| log::set_max_level(LevelFilter::Trace)) .unwrap(); - log::set_max_level(LevelFilter::Warn); + // log::set_max_level(LevelFilter::Warn); 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 - if !scene_path.exists() { - error!("Scene not found at {}", scene_path.display()); - return Err("Scene file not found".into()); - } + let project = UnityProject::from_path(project_root)?; - 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 - match UnityFile::from_path(&scene_path) { - Ok(UnityFile::Scene(scene)) => { + // 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()); @@ -114,10 +114,6 @@ fn main() -> Result<(), Box> { info!("✅ Parsing complete!"); } - Ok(_) => { - error!("File is not a scene"); - return Err("Not a Unity scene file".into()); - } Err(e) => { error!("Parse error: {}", e); return Err(Box::new(e)); diff --git a/resources_output.txt b/resources_output.txt index 0ef37e4..0471527 100644 --- a/resources_output.txt +++ b/resources_output.txt @@ -1,15 +1,10 @@ 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 TypeID: 38 MaxHealth: 0 diff --git a/unity-parser/src/lib.rs b/unity-parser/src/lib.rs index cda849d..8be164f 100644 --- a/unity-parser/src/lib.rs +++ b/unity-parser/src/lib.rs @@ -38,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, diff --git a/unity-parser/src/model/mod.rs b/unity-parser/src/model/mod.rs index c692308..7c5cb22 100644 --- a/unity-parser/src/model/mod.rs +++ b/unity-parser/src/model/mod.rs @@ -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)] @@ -210,3 +212,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) -> crate::Result { + 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) -> crate::Result { + 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) -> crate::Result { + 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) -> crate::Result { + 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) + } + } +} diff --git a/unity-parser/src/parser/guid_resolver.rs b/unity-parser/src/parser/guid_resolver.rs index f6b10b3..60c44da 100644 --- a/unity-parser/src/parser/guid_resolver.rs +++ b/unity-parser/src/parser/guid_resolver.rs @@ -66,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) @@ -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) -> Result { + 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 { 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()) diff --git a/unity-parser/src/parser/mod.rs b/unity-parser/src/parser/mod.rs index c1b9f4f..7a31de1 100644 --- a/unity-parser/src/parser/mod.rs +++ b/unity-parser/src/parser/mod.rs @@ -14,7 +14,7 @@ pub use yaml::split_yaml_documents; 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::{Error, Result}; 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 { + // 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 { + // 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 { + // 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 { let raw_documents = parse_raw_documents(content, type_filter)?; diff --git a/unity-parser/src/parser/prefab_guid_resolver.rs b/unity-parser/src/parser/prefab_guid_resolver.rs index 9344c43..6b1acca 100644 --- a/unity-parser/src/parser/prefab_guid_resolver.rs +++ b/unity-parser/src/parser/prefab_guid_resolver.rs @@ -62,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) @@ -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) -> Result { + 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 { 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())