diff --git a/unity-parser/src/ecs/builder.rs b/unity-parser/src/ecs/builder.rs index 9383ab4..a456707 100644 --- a/unity-parser/src/ecs/builder.rs +++ b/unity-parser/src/ecs/builder.rs @@ -6,7 +6,7 @@ use crate::model::RawDocument; use crate::parser::{GuidResolver, PrefabGuidResolver}; use crate::types::{ yaml_helpers, ComponentContext, FileID, GameObject, LinkingContext, PrefabInstanceComponent, - PrefabResolver, RectTransform, Transform, TypeFilter, UnityComponent, + PrefabResolver, RectTransform, StrippedReference, Transform, TypeFilter, UnityComponent, }; use crate::{Error, Result}; use sparsey::{Entity, World}; @@ -39,7 +39,8 @@ pub fn build_world_from_documents( .register::() .register::() .register::() - .register::(); + .register::() + .register::(); // Register all custom components from inventory for reg in inventory::iter:: { @@ -80,9 +81,35 @@ pub fn build_world_from_documents( } } + // PASS 1.5: Handle stripped components + // Stripped components are references to prefab components that don't have full data in the scene + for doc in documents.iter().filter(|d| d.is_stripped) { + // Create an entity for this stripped reference + let entity = world.create(()); + linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity); + + // Parse and attach the StrippedReference component + if let Some(yaml) = doc.as_mapping() { + let ctx = ComponentContext { + type_id: doc.type_id, + file_id: doc.file_id, + class_name: &doc.class_name, + entity: Some(entity), + linking_ctx: Some(&linking_ctx), + yaml, + guid_resolver, + }; + + if let Some(stripped_ref) = StrippedReference::parse(yaml, &ctx) { + world.insert(entity, (stripped_ref,)); + } + } + } + // PASS 2: Attach components to entities let type_filter = TypeFilter::parse_all(); for doc in documents.iter().filter(|d| { + !d.is_stripped && d.type_id != 1 && d.class_name != "GameObject" && d.type_id != 1001 && d.class_name != "PrefabInstance" }) { @@ -204,9 +231,36 @@ pub fn build_world_from_documents_into( } } + // PASS 1.5: Handle stripped components + // Stripped components are references to prefab components that don't have full data in the scene + for doc in documents.iter().filter(|d| d.is_stripped) { + // Create an entity for this stripped reference + let entity = world.create(()); + linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity); + spawned_entities.push(entity); + + // Parse and attach the StrippedReference component + if let Some(yaml) = doc.as_mapping() { + let ctx = ComponentContext { + type_id: doc.type_id, + file_id: doc.file_id, + class_name: &doc.class_name, + entity: Some(entity), + linking_ctx: Some(&linking_ctx), + yaml, + guid_resolver, + }; + + if let Some(stripped_ref) = StrippedReference::parse(yaml, &ctx) { + world.insert(entity, (stripped_ref,)); + } + } + } + // PASS 2: Attach components to entities let type_filter = TypeFilter::parse_all(); for doc in documents.iter().filter(|d| { + !d.is_stripped && d.type_id != 1 && d.class_name != "GameObject" && d.type_id != 1001 && d.class_name != "PrefabInstance" }) { diff --git a/unity-parser/src/model/mod.rs b/unity-parser/src/model/mod.rs index 7c5cb22..65d04f4 100644 --- a/unity-parser/src/model/mod.rs +++ b/unity-parser/src/model/mod.rs @@ -189,6 +189,9 @@ pub struct RawDocument { /// Raw YAML value (inner mapping after class wrapper) pub yaml: serde_yaml::Value, + + /// Whether this component is stripped (exists in prefab, not in scene) + pub is_stripped: bool, } impl RawDocument { @@ -198,12 +201,14 @@ impl RawDocument { file_id: FileID, class_name: String, yaml: serde_yaml::Value, + is_stripped: bool, ) -> Self { Self { type_id, file_id, class_name, yaml, + is_stripped, } } diff --git a/unity-parser/src/parser/mod.rs b/unity-parser/src/parser/mod.rs index 7a31de1..c071d1a 100644 --- a/unity-parser/src/parser/mod.rs +++ b/unity-parser/src/parser/mod.rs @@ -384,6 +384,7 @@ fn parse_raw_document(raw_doc: &str, type_filter: Option<&TypeFilter>) -> Result FileID::from_i64(tag.file_id), class_name, inner_yaml, + tag.is_stripped, ))) } diff --git a/unity-parser/src/parser/unity_tag.rs b/unity-parser/src/parser/unity_tag.rs index e7981fd..bdc34b8 100644 --- a/unity-parser/src/parser/unity_tag.rs +++ b/unity-parser/src/parser/unity_tag.rs @@ -15,15 +15,19 @@ pub struct UnityTag { /// File ID (the number after &) pub file_id: i64, + + /// Whether this component is stripped (exists in prefab, not in scene) + pub is_stripped: bool, } /// Get the Unity tag regex (compiled once and cached) fn unity_tag_regex() -> &'static Regex { static REGEX: OnceLock = OnceLock::new(); REGEX.get_or_init(|| { - // Matches: --- !u! & + // Matches: --- !u! & [stripped] // Example: --- !u!1 &1866116814460599870 - Regex::new(r"^---\s+!u!(\d+)\s+&(-?\d+)").unwrap() + // Example: --- !u!4 &104494228 stripped + Regex::new(r"^---\s+!u!(\d+)\s+&(-?\d+)(?:\s+(stripped))?").unwrap() }) } @@ -38,6 +42,7 @@ fn unity_tag_regex() -> &'static Regex { /// let tag = parse_unity_tag(doc).unwrap(); /// assert_eq!(tag.type_id, 1); /// assert_eq!(tag.file_id, 12345); +/// assert_eq!(tag.is_stripped, false); /// ``` pub fn parse_unity_tag(document: &str) -> Option { let re = unity_tag_regex(); @@ -52,7 +57,14 @@ pub fn parse_unity_tag(document: &str) -> Option { let type_id = captures.get(1)?.as_str().parse::().ok()?; let file_id = captures.get(2)?.as_str().parse::().ok()?; - Some(UnityTag { type_id, file_id }) + // Check if "stripped" keyword is present + let is_stripped = captures.get(3).is_some(); + + Some(UnityTag { + type_id, + file_id, + is_stripped, + }) } #[cfg(test)] @@ -65,6 +77,7 @@ mod tests { let tag = parse_unity_tag(doc).unwrap(); assert_eq!(tag.type_id, 1); assert_eq!(tag.file_id, 1866116814460599870); + assert_eq!(tag.is_stripped, false); } #[test] @@ -73,6 +86,7 @@ mod tests { let tag = parse_unity_tag(doc).unwrap(); assert_eq!(tag.type_id, 224); assert_eq!(tag.file_id, 8151827567463220614); + assert_eq!(tag.is_stripped, false); } #[test] @@ -81,6 +95,16 @@ mod tests { let tag = parse_unity_tag(doc).unwrap(); assert_eq!(tag.type_id, 114); assert_eq!(tag.file_id, -12345); + assert_eq!(tag.is_stripped, false); + } + + #[test] + fn test_parse_unity_tag_stripped() { + let doc = "--- !u!4 &104494228 stripped\nTransform:\n m_CorrespondingSourceObject: {fileID: 1381906096329791709}"; + let tag = parse_unity_tag(doc).unwrap(); + assert_eq!(tag.type_id, 4); + assert_eq!(tag.file_id, 104494228); + assert_eq!(tag.is_stripped, true); } #[test] diff --git a/unity-parser/src/types/mod.rs b/unity-parser/src/types/mod.rs index e9ccdbc..34afc38 100644 --- a/unity-parser/src/types/mod.rs +++ b/unity-parser/src/types/mod.rs @@ -24,6 +24,6 @@ pub use type_filter::TypeFilter; pub use type_registry::{get_class_name, get_type_id}; pub use unity_types::{ BoxCollider, GameObject, MeshFilter, MeshRenderer, PrefabInstance, PrefabInstanceComponent, - PrefabModification, PrefabResolver, RectTransform, Renderer, Transform, + PrefabModification, PrefabResolver, RectTransform, Renderer, StrippedReference, Transform, }; pub use values::{Color, ExternalRef, FileRef, Quaternion, Vector2, Vector3}; diff --git a/unity-parser/src/types/unity_types/mod.rs b/unity-parser/src/types/unity_types/mod.rs index 63779df..28da36e 100644 --- a/unity-parser/src/types/unity_types/mod.rs +++ b/unity-parser/src/types/unity_types/mod.rs @@ -9,6 +9,7 @@ pub mod mesh_renderer; pub mod prefab_instance; pub mod renderer; pub mod sphere_collider; +pub mod stripped_reference; pub mod transform; pub use box_collider::BoxCollider; @@ -22,4 +23,5 @@ pub use prefab_instance::{ }; pub use renderer::Renderer; pub use sphere_collider::SphereCollider; +pub use stripped_reference::StrippedReference; pub use transform::{RectTransform, Transform}; diff --git a/unity-parser/src/types/unity_types/stripped_reference.rs b/unity-parser/src/types/unity_types/stripped_reference.rs new file mode 100644 index 0000000..0af3c0a --- /dev/null +++ b/unity-parser/src/types/unity_types/stripped_reference.rs @@ -0,0 +1,114 @@ +//! Stripped component reference +//! +//! Stripped components are references to components that exist in prefabs +//! but whose data isn't duplicated in the scene file. + +use crate::types::{yaml_helpers, ComponentContext, FileID, Guid, UnityComponent}; +use sparsey::Entity; + +/// A stripped component reference +/// +/// Stripped components appear in Unity scenes as lightweight references to +/// prefab components. They have the format: +/// ```yaml +/// --- !u!4 &104494228 stripped +/// Transform: +/// m_CorrespondingSourceObject: {fileID: 1381906096329791709, guid: e959fe449ad88a946b99ba9e7e3617ef, type: 3} +/// m_PrefabInstance: {fileID: 104494225} +/// m_PrefabAsset: {fileID: 0} +/// ``` +/// +/// The actual component data lives in the prefab file referenced by the GUID. +#[derive(Debug, Clone)] +pub struct StrippedReference { + /// The Unity type name of this component (e.g., "Transform", "GameObject") + pub component_type: String, + + /// FileID of the corresponding source object in the prefab + pub source_file_id: Option, + + /// GUID of the prefab that contains the actual component data + pub prefab_guid: Option, + + /// FileID of the PrefabInstance this stripped component belongs to + pub prefab_instance: Option, +} + +impl StrippedReference { + /// Create a new StrippedReference + pub fn new( + component_type: String, + source_file_id: Option, + prefab_guid: Option, + prefab_instance: Option, + ) -> Self { + Self { + component_type, + source_file_id, + prefab_guid, + prefab_instance, + } + } + + /// Get the component type + pub fn component_type(&self) -> &str { + &self.component_type + } + + /// Get the source FileID in the prefab + pub fn source_file_id(&self) -> Option { + self.source_file_id + } + + /// Get the prefab GUID + pub fn prefab_guid(&self) -> Option { + self.prefab_guid + } + + /// Get the PrefabInstance FileID + pub fn prefab_instance(&self) -> Option { + self.prefab_instance + } +} + +impl UnityComponent for StrippedReference { + /// Parse a stripped component reference from YAML + /// + /// Extracts the m_CorrespondingSourceObject and m_PrefabInstance references. + fn parse(yaml: &serde_yaml::Mapping, ctx: &ComponentContext) -> Option { + use serde_yaml::Value; + + // Extract m_CorrespondingSourceObject: {fileID: ..., guid: ..., type: 3} + // This reference has both fileID (source object in prefab) and guid (prefab GUID) + let source_obj = yaml + .get(&Value::String("m_CorrespondingSourceObject".to_string())) + .and_then(|v| v.as_mapping()); + + let source_file_id = source_obj + .and_then(|obj| obj.get(&Value::String("fileID".to_string()))) + .and_then(|v| v.as_i64()) + .map(FileID::from_i64); + + let prefab_guid = source_obj + .and_then(|obj| obj.get(&Value::String("guid".to_string()))) + .and_then(|v| v.as_str()) + .and_then(|s| Guid::from_hex(s).ok()); + + // Extract m_PrefabInstance: {fileID: ...} + let prefab_instance_ref = yaml_helpers::get_file_ref(yaml, "m_PrefabInstance"); + let prefab_instance = prefab_instance_ref.map(|r| r.file_id); + + Some(Self { + component_type: ctx.class_name.to_string(), + source_file_id, + prefab_guid, + prefab_instance, + }) + } +} + +impl crate::types::EcsInsertable for StrippedReference { + fn insert_into_world(self, world: &mut sparsey::World, entity: Entity) { + world.insert(entity, (self,)); + } +}