diff --git a/cursebreaker-parser/src/lib.rs b/cursebreaker-parser/src/lib.rs index 1e2ff46..d6b106c 100644 --- a/cursebreaker-parser/src/lib.rs +++ b/cursebreaker-parser/src/lib.rs @@ -38,15 +38,15 @@ pub mod types; // Re-exports pub use error::{Error, Result}; pub use model::{RawDocument, UnityAsset, UnityFile, UnityPrefab, UnityScene}; -pub use parser::{meta::MetaFile, parse_unity_file}; +pub use parser::{find_project_root, meta::MetaFile, parse_unity_file, GuidResolver}; // 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, - ExternalRef, FileID, FileRef, GameObject, LocalID, PrefabInstance, PrefabInstanceComponent, - PrefabModification, PrefabResolver, Quaternion, RectTransform, Transform, TypeFilter, - UnityComponent, UnityReference, Vector2, Vector3, yaml_helpers, + ExternalRef, FileID, FileRef, GameObject, Guid, LocalID, PrefabInstance, + PrefabInstanceComponent, PrefabModification, PrefabResolver, Quaternion, RectTransform, + Transform, TypeFilter, UnityComponent, UnityReference, Vector2, Vector3, yaml_helpers, }; // Re-export the derive macro from the macro crate diff --git a/cursebreaker-parser/src/parser/guid_resolver.rs b/cursebreaker-parser/src/parser/guid_resolver.rs new file mode 100644 index 0000000..b4b6016 --- /dev/null +++ b/cursebreaker-parser/src/parser/guid_resolver.rs @@ -0,0 +1,492 @@ +//! Unity GUID resolution for MonoBehaviour scripts +//! +//! This module resolves Unity GUIDs to their corresponding MonoBehaviour class names +//! by scanning .meta files and parsing C# scripts. +//! +//! # How Unity GUID Resolution Works +//! +//! 1. Every asset has a `.meta` file with a unique GUID +//! 2. MonoBehaviour components reference scripts via GUID in `m_Script` +//! 3. We scan `.cs.meta` files to build a GUID → Script Path mapping +//! 4. We parse the `.cs` files to extract the class name +//! 5. Result: GUID → Class Name mapping for component registration +//! +//! # Example +//! +//! ```no_run +//! use cursebreaker_parser::parser::GuidResolver; +//! use std::path::Path; +//! +//! // Build resolver from Unity project directory +//! let project_path = Path::new("path/to/UnityProject"); +//! let resolver = GuidResolver::from_project(project_path)?; +//! +//! // Resolve a GUID to class name +//! let guid = "091c537484687e9419460cdcd7038234"; +//! if let Some(class_name) = resolver.resolve_class_name(guid) { +//! println!("GUID {} → {}", guid, class_name); +//! } +//! # Ok::<(), cursebreaker_parser::Error>(()) +//! ``` + +use crate::parser::meta::MetaFile; +use crate::types::Guid; +use crate::{Error, Result}; +use regex::Regex; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use walkdir::WalkDir; + +/// Resolves Unity GUIDs to MonoBehaviour class names +/// +/// This struct builds a mapping from GUID to class name by scanning +/// a Unity project's `.cs.meta` files and parsing the corresponding +/// C# scripts to extract class names. +/// +/// Uses a 128-bit `Guid` type for efficient storage and fast comparisons. +#[derive(Debug, Clone)] +pub struct GuidResolver { + /// Map from GUID to MonoBehaviour class name + guid_to_class: HashMap, +} + +impl GuidResolver { + /// Create a new empty GuidResolver + pub fn new() -> Self { + Self { + guid_to_class: HashMap::new(), + } + } + + /// Build a GuidResolver by scanning a Unity project directory + /// + /// This scans for all `.cs.meta` files (MonoBehaviour scripts), + /// parses them to extract GUIDs, then parses the corresponding + /// C# files to extract class names. + /// + /// # Arguments + /// + /// * `project_path` - Path to the Unity project root (containing Assets/ folder) + /// + /// # Examples + /// + /// ```no_run + /// use cursebreaker_parser::parser::GuidResolver; + /// use std::path::Path; + /// + /// let resolver = GuidResolver::from_project(Path::new("MyUnityProject"))?; + /// # Ok::<(), cursebreaker_parser::Error>(()) + /// ``` + pub fn from_project(project_path: impl AsRef) -> Result { + let project_path = project_path.as_ref(); + + // Verify this looks like a Unity project + let assets_dir = project_path.join("Assets"); + if !assets_dir.exists() { + return Err(Error::invalid_format(format!( + "Not a Unity project: missing Assets/ directory at {}", + project_path.display() + ))); + } + + let mut resolver = Self::new(); + + // Walk the Assets directory looking for .cs.meta files + for entry in WalkDir::new(&assets_dir) + .follow_links(false) + .into_iter() + .filter_map(|e| e.ok()) + { + let path = entry.path(); + + // Only process .cs.meta files (MonoBehaviour scripts) + if !is_cs_meta_file(path) { + continue; + } + + // Parse the .meta file to get the GUID + let meta = match MetaFile::from_path(path) { + Ok(meta) => meta, + Err(e) => { + eprintln!("Warning: Failed to parse {}: {}", path.display(), e); + continue; + } + }; + + // Get the corresponding .cs file path + let cs_path = path.with_file_name( + path.file_name() + .and_then(|s| s.to_str()) + .and_then(|s| s.strip_suffix(".meta")) + .unwrap_or(""), + ); + + // Extract the class name from the .cs file + let class_name = match extract_class_name(&cs_path) { + Ok(Some(name)) => name, + Ok(None) => { + // No MonoBehaviour class found in this file + continue; + } + Err(e) => { + eprintln!( + "Warning: Failed to extract class name from {}: {}", + cs_path.display(), + e + ); + continue; + } + }; + + // Parse the GUID string to Guid type + let guid = match Guid::from_hex(meta.guid()) { + Ok(g) => g, + Err(e) => { + eprintln!( + "Warning: Invalid GUID in {}: {}", + path.display(), + e + ); + continue; + } + }; + + // Store the mapping + resolver.guid_to_class.insert(guid, class_name); + } + + Ok(resolver) + } + + /// Resolve a GUID to its class name + /// + /// Accepts either a `&Guid` or a string that can be parsed as a GUID. + /// + /// # Arguments + /// + /// * `guid` - The Unity GUID (e.g., "091c537484687e9419460cdcd7038234" or a `Guid`) + /// + /// # Returns + /// + /// The class name if the GUID is found, otherwise None + /// + /// # Examples + /// + /// ```no_run + /// # use cursebreaker_parser::{GuidResolver, Guid}; + /// # use std::path::Path; + /// # let resolver = GuidResolver::from_project(Path::new("."))?; + /// // Resolve by string + /// if let Some(class_name) = resolver.resolve_class_name("091c537484687e9419460cdcd7038234") { + /// println!("Found class: {}", class_name); + /// } + /// + /// // Resolve by Guid + /// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234")?; + /// if let Some(class_name) = resolver.resolve_class_name(&guid) { + /// println!("Found class: {}", class_name); + /// } + /// # Ok::<(), cursebreaker_parser::Error>(()) + /// ``` + pub fn resolve_class_name(&self, guid: G) -> Option<&str> { + guid.as_guid() + .and_then(|g| self.guid_to_class.get(&g)) + .map(|s| s.as_str()) + } + + /// Get the number of GUID mappings + pub fn len(&self) -> usize { + self.guid_to_class.len() + } + + /// Check if the resolver is empty + pub fn is_empty(&self) -> bool { + self.guid_to_class.is_empty() + } + + /// Insert a GUID → class name mapping manually + /// + /// Useful for testing or adding custom mappings + /// + /// # Examples + /// + /// ``` + /// use cursebreaker_parser::{GuidResolver, Guid}; + /// + /// let mut resolver = GuidResolver::new(); + /// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap(); + /// resolver.insert(guid, "PlaySFX".to_string()); + /// ``` + pub fn insert(&mut self, guid: Guid, class_name: String) { + self.guid_to_class.insert(guid, class_name); + } + + /// Get all resolved GUIDs + /// + /// Returns an iterator over all GUIDs in the resolver. + pub fn guids(&self) -> impl Iterator + '_ { + self.guid_to_class.keys().copied() + } +} + +impl Default for GuidResolver { + fn default() -> Self { + Self::new() + } +} + +/// Trait for types that can be converted to a GUID for lookup +/// +/// This allows the `resolve_class_name` method to accept both `&Guid` and `&str`. +pub trait AsGuid { + /// Convert to a GUID, returning None if conversion fails + fn as_guid(&self) -> Option; +} + +impl AsGuid for Guid { + fn as_guid(&self) -> Option { + Some(*self) + } +} + +impl AsGuid for &Guid { + fn as_guid(&self) -> Option { + Some(**self) + } +} + +impl AsGuid for str { + fn as_guid(&self) -> Option { + Guid::from_hex(self).ok() + } +} + +impl AsGuid for &str { + fn as_guid(&self) -> Option { + Guid::from_hex(self).ok() + } +} + +impl AsGuid for String { + fn as_guid(&self) -> Option { + Guid::from_hex(self).ok() + } +} + +/// Check if a path points to a C# .meta file +fn is_cs_meta_file(path: &Path) -> bool { + path.extension() + .and_then(|s| s.to_str()) + .map(|ext| ext == "meta") + .unwrap_or(false) + && path + .file_name() + .and_then(|s| s.to_str()) + .map(|name| name.ends_with(".cs.meta")) + .unwrap_or(false) +} + +/// Extract the MonoBehaviour class name from a C# script file +/// +/// This looks for patterns like: +/// - `public class ClassName : MonoBehaviour` +/// - `class ClassName : MonoBehaviour` +/// - `public class ClassName:MonoBehaviour` +/// +/// # Arguments +/// +/// * `cs_path` - Path to the .cs file +/// +/// # Returns +/// +/// Ok(Some(class_name)) if a MonoBehaviour class was found +/// Ok(None) if no MonoBehaviour class was found +/// Err if the file couldn't be read +fn extract_class_name(cs_path: &Path) -> Result> { + let content = std::fs::read_to_string(cs_path)?; + + // Regex to match MonoBehaviour class declarations + // Matches: public class ClassName : MonoBehaviour + // Also matches: class ClassName:MonoBehaviour (no space) + // Captures the class name in group 1 + let class_regex = Regex::new( + r"(?:public\s+)?class\s+(\w+)\s*:\s*MonoBehaviour" + ).unwrap(); + + // Find the first MonoBehaviour class in the file + if let Some(captures) = class_regex.captures(&content) { + if let Some(class_name) = captures.get(1) { + return Ok(Some(class_name.as_str().to_string())); + } + } + + Ok(None) +} + +/// Find the Unity project root from any path within the project +/// +/// Searches upward from the given path until it finds a directory +/// containing an "Assets" folder. +/// +/// # Arguments +/// +/// * `path` - Any path within the Unity project (file or directory) +/// +/// # Returns +/// +/// The project root path if found, otherwise an error +/// +/// # Examples +/// +/// ```no_run +/// use cursebreaker_parser::parser::find_project_root; +/// use std::path::Path; +/// +/// let scene_path = Path::new("MyProject/Assets/Scenes/Main.unity"); +/// let project_root = find_project_root(scene_path)?; +/// assert_eq!(project_root.file_name().unwrap(), "MyProject"); +/// # Ok::<(), cursebreaker_parser::Error>(()) +/// ``` +pub fn find_project_root(path: impl AsRef) -> Result { + let path = path.as_ref(); + + // Start from the file's directory (or the directory itself) + let mut current = if path.is_file() { + path.parent().ok_or_else(|| { + Error::invalid_format("Cannot get parent directory") + })? + } else { + path + }; + + // Search upward for Assets/ directory + loop { + let assets_dir = current.join("Assets"); + if assets_dir.exists() && assets_dir.is_dir() { + return Ok(current.to_path_buf()); + } + + // Move up one directory + current = current.parent().ok_or_else(|| { + Error::invalid_format(format!( + "Could not find Unity project root from {}", + path.display() + )) + })?; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_cs_meta_file() { + assert!(is_cs_meta_file(Path::new("PlaySFX.cs.meta"))); + assert!(is_cs_meta_file(Path::new("Assets/Scripts/PlaySFX.cs.meta"))); + assert!(!is_cs_meta_file(Path::new("PlaySFX.cs"))); + assert!(!is_cs_meta_file(Path::new("Scene.unity.meta"))); + assert!(!is_cs_meta_file(Path::new("texture.png.meta"))); + } + + #[test] + fn test_extract_class_name() { + // Test standard MonoBehaviour class + let content = r#" +using UnityEngine; + +public class PlaySFX : MonoBehaviour +{ + public float volume = 1.0f; +} +"#; + let temp_file = std::env::temp_dir().join("test_playsfx.cs"); + std::fs::write(&temp_file, content).unwrap(); + let result = extract_class_name(&temp_file).unwrap(); + assert_eq!(result, Some("PlaySFX".to_string())); + std::fs::remove_file(&temp_file).ok(); + } + + #[test] + fn test_extract_class_name_no_public() { + // Test without public modifier + let content = r#" +using UnityEngine; + +class MyBehaviour : MonoBehaviour +{ + void Start() {} +} +"#; + let temp_file = std::env::temp_dir().join("test_mybehaviour.cs"); + std::fs::write(&temp_file, content).unwrap(); + let result = extract_class_name(&temp_file).unwrap(); + assert_eq!(result, Some("MyBehaviour".to_string())); + std::fs::remove_file(&temp_file).ok(); + } + + #[test] + fn test_extract_class_name_no_space() { + // Test with no space after colon + let content = r#" +using UnityEngine; + +public class TestScript:MonoBehaviour +{ + void Update() {} +} +"#; + let temp_file = std::env::temp_dir().join("test_testscript.cs"); + std::fs::write(&temp_file, content).unwrap(); + let result = extract_class_name(&temp_file).unwrap(); + assert_eq!(result, Some("TestScript".to_string())); + std::fs::remove_file(&temp_file).ok(); + } + + #[test] + fn test_extract_class_name_not_monobehaviour() { + // Test class that doesn't inherit from MonoBehaviour + let content = r#" +using UnityEngine; + +public class HelperClass +{ + public int value; +} +"#; + let temp_file = std::env::temp_dir().join("test_helper.cs"); + std::fs::write(&temp_file, content).unwrap(); + let result = extract_class_name(&temp_file).unwrap(); + assert_eq!(result, None); + std::fs::remove_file(&temp_file).ok(); + } + + #[test] + fn test_guid_resolver_manual() { + let mut resolver = GuidResolver::new(); + assert!(resolver.is_empty()); + + let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap(); + resolver.insert(guid, "PlaySFX".to_string()); + + assert_eq!(resolver.len(), 1); + + // Test resolving by string + assert_eq!( + resolver.resolve_class_name("091c537484687e9419460cdcd7038234"), + Some("PlaySFX") + ); + + // Test resolving by Guid + assert_eq!( + resolver.resolve_class_name(&guid), + Some("PlaySFX") + ); + + // Test nonexistent GUID + assert_eq!( + resolver.resolve_class_name("00000000000000000000000000000000"), + None + ); + } +} diff --git a/cursebreaker-parser/src/parser/mod.rs b/cursebreaker-parser/src/parser/mod.rs index ed11924..500e258 100644 --- a/cursebreaker-parser/src/parser/mod.rs +++ b/cursebreaker-parser/src/parser/mod.rs @@ -1,9 +1,11 @@ //! Unity YAML parsing module +pub mod guid_resolver; pub mod meta; mod unity_tag; mod yaml; +pub use guid_resolver::{find_project_root, GuidResolver}; pub use meta::{get_meta_path, MetaFile}; pub use unity_tag::{parse_unity_tag, UnityTag}; pub use yaml::split_yaml_documents; diff --git a/cursebreaker-parser/src/types/guid.rs b/cursebreaker-parser/src/types/guid.rs new file mode 100644 index 0000000..ee4d63d --- /dev/null +++ b/cursebreaker-parser/src/types/guid.rs @@ -0,0 +1,255 @@ +//! Unity GUID type +//! +//! Unity uses 128-bit GUIDs to uniquely identify assets across a project. +//! GUIDs are stored as 32 hexadecimal characters (e.g., "091c537484687e9419460cdcd7038234"). +//! +//! This module provides a type-safe, efficient representation of Unity GUIDs +//! using a 128-bit integer for fast comparisons and minimal memory footprint. + +use crate::{Error, Result}; +use std::fmt; +use std::str::FromStr; + +/// A Unity GUID represented as a 128-bit integer +/// +/// Unity GUIDs are 32 hexadecimal characters representing a 128-bit value. +/// This type stores the GUID as a `u128` for efficient storage and comparison. +/// +/// # Example +/// +/// ``` +/// use cursebreaker_parser::Guid; +/// use std::str::FromStr; +/// +/// // Parse from string +/// let guid = Guid::from_str("091c537484687e9419460cdcd7038234").unwrap(); +/// +/// // Convert back to string +/// assert_eq!(guid.to_string(), "091c537484687e9419460cdcd7038234"); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Guid(u128); + +impl Guid { + /// Create a new GUID from a 128-bit integer + /// + /// # Example + /// + /// ``` + /// use cursebreaker_parser::Guid; + /// + /// let guid = Guid::from_u128(0x091c537484687e9419460cdcd7038234); + /// ``` + pub const fn from_u128(value: u128) -> Self { + Guid(value) + } + + /// Get the underlying 128-bit integer value + /// + /// # Example + /// + /// ``` + /// use cursebreaker_parser::Guid; + /// + /// let guid = Guid::from_u128(42); + /// assert_eq!(guid.as_u128(), 42); + /// ``` + pub const fn as_u128(&self) -> u128 { + self.0 + } + + /// Create a GUID from a hex string + /// + /// The string must be exactly 32 hexadecimal characters. + /// + /// # Errors + /// + /// Returns an error if the string is not valid hex or not 32 characters. + /// + /// # Example + /// + /// ``` + /// use cursebreaker_parser::Guid; + /// + /// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap(); + /// ``` + pub fn from_hex(s: &str) -> Result { + // Unity GUIDs are always 32 hex characters (128 bits) + if s.len() != 32 { + return Err(Error::invalid_format(format!( + "GUID must be 32 hex characters, got {}", + s.len() + ))); + } + + // Parse hex string to u128 + let value = u128::from_str_radix(s, 16).map_err(|e| { + Error::invalid_format(format!("Invalid hex in GUID '{}': {}", s, e)) + })?; + + Ok(Guid(value)) + } + + /// Convert the GUID to a lowercase hex string + /// + /// Returns a 32-character hex string representing the GUID. + /// + /// # Example + /// + /// ``` + /// use cursebreaker_parser::Guid; + /// use std::str::FromStr; + /// + /// let guid = Guid::from_str("091c537484687e9419460cdcd7038234").unwrap(); + /// assert_eq!(guid.to_hex(), "091c537484687e9419460cdcd7038234"); + /// ``` + pub fn to_hex(&self) -> String { + format!("{:032x}", self.0) + } + + /// A zero GUID (all bits set to 0) + pub const ZERO: Guid = Guid(0); +} + +impl FromStr for Guid { + type Err = Error; + + fn from_str(s: &str) -> Result { + Self::from_hex(s) + } +} + +impl fmt::Display for Guid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:032x}", self.0) + } +} + +impl From for Guid { + fn from(value: u128) -> Self { + Guid(value) + } +} + +impl From for u128 { + fn from(guid: Guid) -> Self { + guid.0 + } +} + +// Implement serde serialization if serde feature is enabled +#[cfg(feature = "serde")] +impl serde::Serialize for Guid { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_hex()) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Guid { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Guid::from_hex(&s).map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_guid_from_hex() { + let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap(); + assert_eq!(guid.to_hex(), "091c537484687e9419460cdcd7038234"); + } + + #[test] + fn test_guid_from_str() { + let guid: Guid = "091c537484687e9419460cdcd7038234".parse().unwrap(); + assert_eq!(guid.to_string(), "091c537484687e9419460cdcd7038234"); + } + + #[test] + fn test_guid_invalid_length() { + assert!(Guid::from_hex("123").is_err()); + assert!(Guid::from_hex("091c537484687e9419460cdcd70382341234").is_err()); + } + + #[test] + fn test_guid_invalid_hex() { + assert!(Guid::from_hex("091c537484687e9419460cdcd703823g").is_err()); + assert!(Guid::from_hex("ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ").is_err()); + } + + #[test] + fn test_guid_zero() { + let zero = Guid::ZERO; + assert_eq!(zero.as_u128(), 0); + assert_eq!(zero.to_hex(), "00000000000000000000000000000000"); + } + + #[test] + fn test_guid_from_u128() { + let value: u128 = 0x091c537484687e9419460cdcd7038234; + let guid = Guid::from_u128(value); + assert_eq!(guid.as_u128(), value); + assert_eq!(guid.to_hex(), "091c537484687e9419460cdcd7038234"); + } + + #[test] + fn test_guid_equality() { + let guid1 = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap(); + let guid2 = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap(); + let guid3 = Guid::from_hex("091c537484687e9419460cdcd7038235").unwrap(); + + assert_eq!(guid1, guid2); + assert_ne!(guid1, guid3); + } + + #[test] + fn test_guid_hash() { + use std::collections::HashSet; + + let mut set = HashSet::new(); + let guid1 = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap(); + let guid2 = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap(); + let guid3 = Guid::from_hex("091c537484687e9419460cdcd7038235").unwrap(); + + set.insert(guid1); + set.insert(guid2); // Duplicate, shouldn't increase size + set.insert(guid3); + + assert_eq!(set.len(), 2); + } + + #[test] + fn test_guid_ordering() { + let guid1 = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap(); + let guid2 = Guid::from_hex("091c537484687e9419460cdcd7038235").unwrap(); + + assert!(guid1 < guid2); + assert!(guid2 > guid1); + } + + #[test] + fn test_guid_uppercase_hex() { + // Unity GUIDs are lowercase, but we should handle uppercase too + let guid = Guid::from_hex("091C537484687E9419460CDCD7038234").unwrap(); + // Output is always lowercase + assert_eq!(guid.to_hex(), "091c537484687e9419460cdcd7038234"); + } + + #[test] + fn test_guid_conversion() { + let value: u128 = 12345678901234567890; + let guid: Guid = value.into(); + let back: u128 = guid.into(); + assert_eq!(value, back); + } +} diff --git a/cursebreaker-parser/src/types/mod.rs b/cursebreaker-parser/src/types/mod.rs index eb2a03b..393fbf9 100644 --- a/cursebreaker-parser/src/types/mod.rs +++ b/cursebreaker-parser/src/types/mod.rs @@ -6,6 +6,7 @@ mod component; mod game_object; +mod guid; mod ids; mod prefab_instance; mod reference; @@ -19,6 +20,7 @@ pub use component::{ LinkingContext, UnityComponent, }; pub use game_object::GameObject; +pub use guid::Guid; pub use ids::{FileID, LocalID}; pub use prefab_instance::{ PrefabInstance, PrefabInstanceComponent, PrefabModification, PrefabResolver, diff --git a/cursebreaker-parser/tests/integration_tests.rs b/cursebreaker-parser/tests/integration_tests.rs index 4ec7657..c05628e 100644 --- a/cursebreaker-parser/tests/integration_tests.rs +++ b/cursebreaker-parser/tests/integration_tests.rs @@ -1,6 +1,6 @@ //! Integration tests for parsing real Unity projects -use cursebreaker_parser::UnityFile; +use cursebreaker_parser::{GuidResolver, UnityFile}; use std::path::{Path, PathBuf}; use std::process::Command; use std::time::Instant; @@ -400,3 +400,77 @@ fn benchmark_parsing() { stats.print_summary(); } + +/// Test GUID resolution for MonoBehaviour scripts +#[test] +fn test_guid_resolution() { + let project_path = match clone_test_project(&TestProject::VR_HORROR) { + Ok(path) => path, + Err(e) => { + eprintln!("Failed to clone project: {}", e); + eprintln!("Skipping GUID resolution test (git may not be available)"); + return; + } + }; + + println!("\n{}", "=".repeat(60)); + println!("Testing GUID Resolution"); + println!("{}", "=".repeat(60)); + + // Build GUID resolver from project + println!("\nBuilding GuidResolver from project..."); + let resolver = GuidResolver::from_project(&project_path) + .expect("Should successfully build GuidResolver"); + + println!(" ✓ Found {} GUID mappings", resolver.len()); + assert!( + !resolver.is_empty(), + "Should find at least some C# scripts with GUIDs" + ); + + // Test the known PlaySFX GUID from the roadmap + let playsfx_guid_str = "091c537484687e9419460cdcd7038234"; + println!("\nTesting known GUID resolution:"); + println!(" GUID: {}", playsfx_guid_str); + + // Test resolution by string + match resolver.resolve_class_name(playsfx_guid_str) { + Some(class_name) => { + println!(" ✓ Resolved by string to: {}", class_name); + assert_eq!( + class_name, "PlaySFX", + "PlaySFX GUID should resolve to 'PlaySFX' class name" + ); + } + None => { + panic!("Failed to resolve PlaySFX GUID. Available GUIDs: {:?}", + resolver.guids().take(5).collect::>()); + } + } + + // Test resolution by Guid type + use cursebreaker_parser::Guid; + let playsfx_guid = Guid::from_hex(playsfx_guid_str).unwrap(); + match resolver.resolve_class_name(&playsfx_guid) { + Some(class_name) => { + println!(" ✓ Resolved by Guid type to: {}", class_name); + assert_eq!(class_name, "PlaySFX"); + } + None => panic!("Failed to resolve PlaySFX GUID by Guid type"), + } + + // Show sample of resolved GUIDs + println!("\nSample of resolved GUIDs:"); + for guid in resolver.guids().take(5) { + if let Some(class_name) = resolver.resolve_class_name(&guid) { + println!(" {} → {}", guid, class_name); + } + } + + // Demonstrate memory efficiency + println!("\nMemory efficiency:"); + println!(" Each Guid: {} bytes (u128)", std::mem::size_of::()); + println!(" Each String GUID: ~{} bytes (heap allocated)", 32 + std::mem::size_of::()); + + println!("\n{}", "=".repeat(60)); +}