stripped components

This commit is contained in:
2026-01-05 10:18:35 +00:00
parent 1867c5559b
commit dea28f5b9c
7 changed files with 206 additions and 6 deletions

View File

@@ -6,7 +6,7 @@ use crate::model::RawDocument;
use crate::parser::{GuidResolver, PrefabGuidResolver}; use crate::parser::{GuidResolver, PrefabGuidResolver};
use crate::types::{ use crate::types::{
yaml_helpers, ComponentContext, FileID, GameObject, LinkingContext, PrefabInstanceComponent, yaml_helpers, ComponentContext, FileID, GameObject, LinkingContext, PrefabInstanceComponent,
PrefabResolver, RectTransform, Transform, TypeFilter, UnityComponent, PrefabResolver, RectTransform, StrippedReference, Transform, TypeFilter, UnityComponent,
}; };
use crate::{Error, Result}; use crate::{Error, Result};
use sparsey::{Entity, World}; use sparsey::{Entity, World};
@@ -39,7 +39,8 @@ pub fn build_world_from_documents(
.register::<GameObject>() .register::<GameObject>()
.register::<Transform>() .register::<Transform>()
.register::<RectTransform>() .register::<RectTransform>()
.register::<PrefabInstanceComponent>(); .register::<PrefabInstanceComponent>()
.register::<StrippedReference>();
// Register all custom components from inventory // Register all custom components from inventory
for reg in inventory::iter::<crate::types::ComponentRegistration> { for reg in inventory::iter::<crate::types::ComponentRegistration> {
@@ -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 // PASS 2: Attach components to entities
let type_filter = TypeFilter::parse_all(); let type_filter = TypeFilter::parse_all();
for doc in documents.iter().filter(|d| { for doc in documents.iter().filter(|d| {
!d.is_stripped &&
d.type_id != 1 && d.class_name != "GameObject" && d.type_id != 1 && d.class_name != "GameObject" &&
d.type_id != 1001 && d.class_name != "PrefabInstance" 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 // PASS 2: Attach components to entities
let type_filter = TypeFilter::parse_all(); let type_filter = TypeFilter::parse_all();
for doc in documents.iter().filter(|d| { for doc in documents.iter().filter(|d| {
!d.is_stripped &&
d.type_id != 1 && d.class_name != "GameObject" && d.type_id != 1 && d.class_name != "GameObject" &&
d.type_id != 1001 && d.class_name != "PrefabInstance" d.type_id != 1001 && d.class_name != "PrefabInstance"
}) { }) {

View File

@@ -189,6 +189,9 @@ pub struct RawDocument {
/// Raw YAML value (inner mapping after class wrapper) /// Raw YAML value (inner mapping after class wrapper)
pub yaml: serde_yaml::Value, pub yaml: serde_yaml::Value,
/// Whether this component is stripped (exists in prefab, not in scene)
pub is_stripped: bool,
} }
impl RawDocument { impl RawDocument {
@@ -198,12 +201,14 @@ impl RawDocument {
file_id: FileID, file_id: FileID,
class_name: String, class_name: String,
yaml: serde_yaml::Value, yaml: serde_yaml::Value,
is_stripped: bool,
) -> Self { ) -> Self {
Self { Self {
type_id, type_id,
file_id, file_id,
class_name, class_name,
yaml, yaml,
is_stripped,
} }
} }

View File

@@ -384,6 +384,7 @@ fn parse_raw_document(raw_doc: &str, type_filter: Option<&TypeFilter>) -> Result
FileID::from_i64(tag.file_id), FileID::from_i64(tag.file_id),
class_name, class_name,
inner_yaml, inner_yaml,
tag.is_stripped,
))) )))
} }

View File

@@ -15,15 +15,19 @@ pub struct UnityTag {
/// File ID (the number after &) /// File ID (the number after &)
pub file_id: i64, 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) /// Get the Unity tag regex (compiled once and cached)
fn unity_tag_regex() -> &'static Regex { fn unity_tag_regex() -> &'static Regex {
static REGEX: OnceLock<Regex> = OnceLock::new(); static REGEX: OnceLock<Regex> = OnceLock::new();
REGEX.get_or_init(|| { REGEX.get_or_init(|| {
// Matches: --- !u!<type_id> &<file_id> // Matches: --- !u!<type_id> &<file_id> [stripped]
// Example: --- !u!1 &1866116814460599870 // 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(); /// let tag = parse_unity_tag(doc).unwrap();
/// assert_eq!(tag.type_id, 1); /// assert_eq!(tag.type_id, 1);
/// assert_eq!(tag.file_id, 12345); /// assert_eq!(tag.file_id, 12345);
/// assert_eq!(tag.is_stripped, false);
/// ``` /// ```
pub fn parse_unity_tag(document: &str) -> Option<UnityTag> { pub fn parse_unity_tag(document: &str) -> Option<UnityTag> {
let re = unity_tag_regex(); let re = unity_tag_regex();
@@ -52,7 +57,14 @@ pub fn parse_unity_tag(document: &str) -> Option<UnityTag> {
let type_id = captures.get(1)?.as_str().parse::<u32>().ok()?; let type_id = captures.get(1)?.as_str().parse::<u32>().ok()?;
let file_id = captures.get(2)?.as_str().parse::<i64>().ok()?; let file_id = captures.get(2)?.as_str().parse::<i64>().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)] #[cfg(test)]
@@ -65,6 +77,7 @@ mod tests {
let tag = parse_unity_tag(doc).unwrap(); let tag = parse_unity_tag(doc).unwrap();
assert_eq!(tag.type_id, 1); assert_eq!(tag.type_id, 1);
assert_eq!(tag.file_id, 1866116814460599870); assert_eq!(tag.file_id, 1866116814460599870);
assert_eq!(tag.is_stripped, false);
} }
#[test] #[test]
@@ -73,6 +86,7 @@ mod tests {
let tag = parse_unity_tag(doc).unwrap(); let tag = parse_unity_tag(doc).unwrap();
assert_eq!(tag.type_id, 224); assert_eq!(tag.type_id, 224);
assert_eq!(tag.file_id, 8151827567463220614); assert_eq!(tag.file_id, 8151827567463220614);
assert_eq!(tag.is_stripped, false);
} }
#[test] #[test]
@@ -81,6 +95,16 @@ mod tests {
let tag = parse_unity_tag(doc).unwrap(); let tag = parse_unity_tag(doc).unwrap();
assert_eq!(tag.type_id, 114); assert_eq!(tag.type_id, 114);
assert_eq!(tag.file_id, -12345); 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] #[test]

View File

@@ -24,6 +24,6 @@ pub use type_filter::TypeFilter;
pub use type_registry::{get_class_name, get_type_id}; pub use type_registry::{get_class_name, get_type_id};
pub use unity_types::{ pub use unity_types::{
BoxCollider, GameObject, MeshFilter, MeshRenderer, PrefabInstance, PrefabInstanceComponent, 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}; pub use values::{Color, ExternalRef, FileRef, Quaternion, Vector2, Vector3};

View File

@@ -9,6 +9,7 @@ pub mod mesh_renderer;
pub mod prefab_instance; pub mod prefab_instance;
pub mod renderer; pub mod renderer;
pub mod sphere_collider; pub mod sphere_collider;
pub mod stripped_reference;
pub mod transform; pub mod transform;
pub use box_collider::BoxCollider; pub use box_collider::BoxCollider;
@@ -22,4 +23,5 @@ pub use prefab_instance::{
}; };
pub use renderer::Renderer; pub use renderer::Renderer;
pub use sphere_collider::SphereCollider; pub use sphere_collider::SphereCollider;
pub use stripped_reference::StrippedReference;
pub use transform::{RectTransform, Transform}; pub use transform::{RectTransform, Transform};

View File

@@ -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<FileID>,
/// GUID of the prefab that contains the actual component data
pub prefab_guid: Option<Guid>,
/// FileID of the PrefabInstance this stripped component belongs to
pub prefab_instance: Option<FileID>,
}
impl StrippedReference {
/// Create a new StrippedReference
pub fn new(
component_type: String,
source_file_id: Option<FileID>,
prefab_guid: Option<Guid>,
prefab_instance: Option<FileID>,
) -> 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<FileID> {
self.source_file_id
}
/// Get the prefab GUID
pub fn prefab_guid(&self) -> Option<Guid> {
self.prefab_guid
}
/// Get the PrefabInstance FileID
pub fn prefab_instance(&self) -> Option<FileID> {
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<Self> {
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,));
}
}