//! Core data model for Unity files //! //! This module provides the fundamental types for representing parsed Unity files. //! 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::{Path, PathBuf}; /// A parsed Unity file - can be a Scene, Prefab, or Asset #[derive(Debug)] pub enum UnityFile { /// Scene file (.unity) with fully-parsed ECS World Scene(UnityScene), /// Prefab file (.prefab) with raw YAML for instancing Prefab(UnityPrefab), /// Asset file (.asset) with raw YAML Asset(UnityAsset), } impl UnityFile { /// Parse a Unity file from the given path pub fn from_path(path: impl Into) -> crate::Result { let path = path.into(); crate::parser::parse_unity_file(&path) } /// Get the file path pub fn path(&self) -> &PathBuf { match self { UnityFile::Scene(s) => &s.path, UnityFile::Prefab(p) => &p.path, UnityFile::Asset(a) => &a.path, } } /// Check if this is a scene pub fn is_scene(&self) -> bool { matches!(self, UnityFile::Scene(_)) } /// Check if this is a prefab pub fn is_prefab(&self) -> bool { matches!(self, UnityFile::Prefab(_)) } /// Check if this is an asset pub fn is_asset(&self) -> bool { matches!(self, UnityFile::Asset(_)) } } /// A Unity scene with fully-parsed ECS World /// /// Scenes contain GameObjects and Components parsed into a Sparsey ECS world. /// The entity_map provides mapping from Unity FileIDs to ECS entities. #[derive(Debug)] pub struct UnityScene { /// Path to the scene file pub path: PathBuf, /// ECS World containing all entities and components pub world: World, /// Mapping from Unity FileID to ECS Entity pub entity_map: HashMap, } impl UnityScene { /// Create a new UnityScene pub fn new(path: PathBuf, world: World, entity_map: HashMap) -> Self { Self { path, world, entity_map, } } /// Get an entity by its Unity FileID pub fn get_entity(&self, file_id: FileID) -> Option { self.entity_map.get(&file_id).copied() } } /// A Unity prefab with raw YAML for instancing /// /// Prefabs are kept as raw YAML documents to enable efficient cloning and /// value overriding during instantiation into scenes. #[derive(Debug, Clone)] pub struct UnityPrefab { /// Path to the prefab file pub path: PathBuf, /// Raw YAML documents that make up this prefab pub documents: Vec, } impl UnityPrefab { /// Create a new UnityPrefab pub fn new(path: PathBuf, documents: Vec) -> Self { Self { path, documents } } /// Get a document by its FileID pub fn get_document(&self, file_id: FileID) -> Option<&RawDocument> { self.documents.iter().find(|doc| doc.file_id == file_id) } /// Get all documents of a specific type pub fn get_documents_by_type(&self, type_id: u32) -> Vec<&RawDocument> { self.documents .iter() .filter(|doc| doc.type_id == type_id) .collect() } /// Get all documents with a specific class name pub fn get_documents_by_class(&self, class_name: &str) -> Vec<&RawDocument> { self.documents .iter() .filter(|doc| doc.class_name == class_name) .collect() } /// Create a new instance of this prefab for spawning into a scene /// /// This clones the prefab's documents and prepares them for instantiation /// with unique FileIDs to avoid collisions. /// /// # Returns /// A `PrefabInstance` that can be customized with overrides and spawned /// /// # Example /// ```ignore /// let mut instance = prefab.instantiate(None); /// instance.override_value(file_id, "m_Name", "Player1".into())?; /// let entities = instance.spawn_into(&mut world, &mut entity_map)?; /// ``` pub fn instantiate(&self, file_id_counter: Option>>) -> crate::types::PrefabInstance { crate::types::PrefabInstance::new(self, file_id_counter) } } /// A Unity asset file with raw YAML /// /// Assets (like ScriptableObjects) are handled similarly to prefabs. #[derive(Debug, Clone)] pub struct UnityAsset { /// Path to the asset file pub path: PathBuf, /// Raw YAML documents that make up this asset pub documents: Vec, } impl UnityAsset { /// Create a new UnityAsset pub fn new(path: PathBuf, documents: Vec) -> Self { Self { path, documents } } /// Get a document by its FileID pub fn get_document(&self, file_id: FileID) -> Option<&RawDocument> { self.documents.iter().find(|doc| doc.file_id == file_id) } } /// A raw YAML document with Unity metadata /// /// This represents a single Unity object (GameObject, Component, etc.) with /// its metadata (type_id, file_id, class_name) and raw YAML content. /// /// The `yaml` field contains the inner YAML mapping (the contents after the /// class name wrapper like `GameObject: { ... }`). #[derive(Debug, Clone)] pub struct RawDocument { /// Unity type ID (from !u!N tag) pub type_id: u32, /// File ID (from &ID anchor) pub file_id: FileID, /// Class name (e.g., "GameObject", "Transform", "RectTransform") pub class_name: String, /// Raw YAML value (inner mapping after class wrapper) pub yaml: serde_yaml::Value, /// Whether this component is stripped (exists in prefab, not in scene) pub is_stripped: bool, } impl RawDocument { /// Create a new RawDocument pub fn new( type_id: u32, file_id: FileID, class_name: String, yaml: serde_yaml::Value, is_stripped: bool, ) -> Self { Self { type_id, file_id, class_name, yaml, is_stripped, } } /// Get the YAML as a mapping, if it is one pub fn as_mapping(&self) -> Option<&serde_yaml::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) -> 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 scene using the pre-built GUID resolvers with optional type filtering /// /// # Arguments /// /// * `path` - Path to the scene file (relative to project root or absolute) /// * `type_filter` - Optional filter for Unity types and MonoBehaviour class names /// /// # Example /// /// ```no_run /// # use unity_parser::{UnityProject, TypeFilter}; /// # let project = UnityProject::from_path("/home/user/repos/CBAssets")?; /// let filter = TypeFilter::new( /// vec!["GameObject", "Transform"], /// vec!["InteractableResource"] /// ); /// let scene = project.parse_scene_filtered("_GameAssets/Scenes/Tiles/10_3.unity", Some(&filter))?; /// println!("Scene has {} entities", scene.entity_map.len()); /// # Ok::<(), unity_parser::Error>(()) /// ``` pub fn parse_scene_filtered( &self, path: impl AsRef, type_filter: Option<&crate::types::TypeFilter> ) -> crate::Result { let path = self.resolve_path(path.as_ref()); crate::parser::parse_scene_with_project_filtered(&path, self, type_filter) } /// 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) } } }