This commit is contained in:
2025-12-30 23:13:12 +09:00
parent 8c4cb4442c
commit bb8b91345c
13 changed files with 1816 additions and 14 deletions

View File

@@ -35,9 +35,9 @@ fn main() {
println!("Found {} GameObjects:", game_objects.len()); println!("Found {} GameObjects:", game_objects.len());
for go in game_objects { for go in game_objects {
if let Some(go_props) = go.get("GameObject") { if let Some(go_props) = go.get("GameObject") {
if let Some(props) = go_props.as_mapping() { if let Some(props) = go_props.as_object() {
if let Some(name) = props.get(&serde_yaml::Value::String("m_Name".to_string())) { if let Some(name) = props.get("m_Name").and_then(|v| v.as_str()) {
println!(" - {}", name.as_str().unwrap_or("Unknown")); println!(" - {}", name);
} }
} }
} }

View File

@@ -46,6 +46,14 @@ pub enum Error {
/// Type conversion error /// Type conversion error
#[error("Type conversion error: expected {expected}, found {found}")] #[error("Type conversion error: expected {expected}, found {found}")]
TypeMismatch { expected: String, found: String }, TypeMismatch { expected: String, found: String },
/// Property value conversion error
#[error("Failed to convert property value from {from} to {to}")]
PropertyConversion { from: String, to: String },
/// Invalid property path
#[error("Invalid property path: {0}")]
InvalidPropertyPath(String),
} }
impl Error { impl Error {

View File

@@ -19,8 +19,15 @@
pub mod error; pub mod error;
pub mod model; pub mod model;
pub mod parser; pub mod parser;
pub mod property;
pub mod types;
// Re-exports // Re-exports
pub use error::{Error, Result}; pub use error::{Error, Result};
pub use model::{UnityDocument, UnityFile}; pub use model::{UnityDocument, UnityFile};
pub use parser::parse_unity_file; pub use parser::parse_unity_file;
pub use property::PropertyValue;
pub use types::{
Color, Component, ExternalRef, FileID, FileRef, GameObject, GenericComponent, LocalID,
Quaternion, RectTransform, Transform, Vector2, Vector3,
};

View File

@@ -1,3 +1,5 @@
use crate::property::PropertyValue;
use crate::types::{Color, FileID, Quaternion, Vector2, Vector3};
use indexmap::IndexMap; use indexmap::IndexMap;
use std::path::PathBuf; use std::path::PathBuf;
@@ -27,7 +29,7 @@ impl UnityFile {
} }
/// Get a document by its file ID /// Get a document by its file ID
pub fn get_document(&self, file_id: i64) -> Option<&UnityDocument> { pub fn get_document(&self, file_id: FileID) -> Option<&UnityDocument> {
self.documents.iter().find(|doc| doc.file_id == file_id) self.documents.iter().find(|doc| doc.file_id == file_id)
} }
@@ -55,7 +57,7 @@ pub struct UnityDocument {
pub type_id: u32, pub type_id: u32,
/// File ID (from &ID anchor) /// File ID (from &ID anchor)
pub file_id: i64, pub file_id: FileID,
/// Class name (e.g., "GameObject", "Transform", "RectTransform") /// Class name (e.g., "GameObject", "Transform", "RectTransform")
pub class_name: String, pub class_name: String,
@@ -66,7 +68,7 @@ pub struct UnityDocument {
impl UnityDocument { impl UnityDocument {
/// Create a new UnityDocument /// Create a new UnityDocument
pub fn new(type_id: u32, file_id: i64, class_name: String) -> Self { pub fn new(type_id: u32, file_id: FileID, class_name: String) -> Self {
Self { Self {
type_id, type_id,
file_id, file_id,
@@ -76,10 +78,67 @@ impl UnityDocument {
} }
/// Get a property value by key /// Get a property value by key
pub fn get(&self, key: &str) -> Option<&serde_yaml::Value> { pub fn get(&self, key: &str) -> Option<&PropertyValue> {
self.properties.get(key) self.properties.get(key)
} }
/// Get a property value as a string
pub fn get_string(&self, key: &str) -> Option<&str> {
self.get(key).and_then(|v| v.as_str())
}
/// Get a property value as an i64
pub fn get_i64(&self, key: &str) -> Option<i64> {
self.get(key).and_then(|v| v.as_i64())
}
/// Get a property value as an f64
pub fn get_f64(&self, key: &str) -> Option<f64> {
self.get(key).and_then(|v| v.as_f64())
}
/// Get a property value as a bool
pub fn get_bool(&self, key: &str) -> Option<bool> {
self.get(key).and_then(|v| v.as_bool())
}
/// Get a property value as a Vector2
pub fn get_vector2(&self, key: &str) -> Option<&Vector2> {
self.get(key).and_then(|v| v.as_vector2())
}
/// Get a property value as a Vector3
pub fn get_vector3(&self, key: &str) -> Option<&Vector3> {
self.get(key).and_then(|v| v.as_vector3())
}
/// Get a property value as a Color
pub fn get_color(&self, key: &str) -> Option<&Color> {
self.get(key).and_then(|v| v.as_color())
}
/// Get a property value as a Quaternion
pub fn get_quaternion(&self, key: &str) -> Option<&Quaternion> {
self.get(key).and_then(|v| v.as_quaternion())
}
/// Get a property value as a FileID
pub fn get_file_ref(&self, key: &str) -> Option<FileID> {
self.get(key)
.and_then(|v| v.as_file_ref())
.map(|r| r.file_id)
}
/// Get a property value as an array
pub fn get_array(&self, key: &str) -> Option<&Vec<PropertyValue>> {
self.get(key).and_then(|v| v.as_array())
}
/// Get a property value as an object
pub fn get_object(&self, key: &str) -> Option<&IndexMap<String, PropertyValue>> {
self.get(key).and_then(|v| v.as_object())
}
/// Check if this is a GameObject /// Check if this is a GameObject
pub fn is_game_object(&self) -> bool { pub fn is_game_object(&self) -> bool {
self.class_name == "GameObject" || self.type_id == 1 self.class_name == "GameObject" || self.type_id == 1
@@ -91,5 +150,5 @@ impl UnityDocument {
} }
} }
/// Property map type (ordered map of string keys to YAML values) /// Property map type (ordered map of string keys to typed property values)
pub type PropertyMap = IndexMap<String, serde_yaml::Value>; pub type PropertyMap = IndexMap<String, PropertyValue>;

View File

@@ -6,6 +6,7 @@ mod yaml;
pub use unity_tag::{UnityTag, parse_unity_tag}; pub use unity_tag::{UnityTag, parse_unity_tag};
pub use yaml::split_yaml_documents; pub use yaml::split_yaml_documents;
use crate::property::convert_yaml_value;
use crate::{Error, Result, UnityDocument, UnityFile}; use crate::{Error, Result, UnityDocument, UnityFile};
use std::path::Path; use std::path::Path;
@@ -74,10 +75,14 @@ fn parse_document(raw_doc: &str) -> Result<Option<UnityDocument>> {
} else { } else {
match serde_yaml::from_str::<serde_yaml::Value>(yaml_content) { match serde_yaml::from_str::<serde_yaml::Value>(yaml_content) {
Ok(serde_yaml::Value::Mapping(map)) => { Ok(serde_yaml::Value::Mapping(map)) => {
// Convert to IndexMap // Convert to IndexMap with PropertyValue
map.into_iter() map.into_iter()
.filter_map(|(k, v)| { .filter_map(|(k, v)| {
k.as_str().map(|s| (s.to_string(), v)) k.as_str().and_then(|s| {
convert_yaml_value(&v)
.ok()
.map(|pv| (s.to_string(), pv))
})
}) })
.collect() .collect()
} }
@@ -95,7 +100,7 @@ fn parse_document(raw_doc: &str) -> Result<Option<UnityDocument>> {
Ok(Some(UnityDocument { Ok(Some(UnityDocument {
type_id: tag.type_id, type_id: tag.type_id,
file_id: tag.file_id, file_id: crate::types::FileID::from_i64(tag.file_id),
class_name, class_name,
properties, properties,
})) }))

531
src/property/mod.rs Normal file
View File

@@ -0,0 +1,531 @@
//! Property value types and conversion
//!
//! This module provides the `PropertyValue` enum which represents
//! typed Unity property values, and conversion logic from YAML values.
use crate::types::{Color, ExternalRef, FileID, FileRef, Quaternion, Vector2, Vector3};
use crate::Error;
use indexmap::IndexMap;
use std::fmt;
/// A typed property value in a Unity object
///
/// This enum represents all possible value types that can appear
/// in Unity YAML files, including Unity-specific types like Vector3 and Color.
#[derive(Debug, Clone, PartialEq)]
pub enum PropertyValue {
/// Integer value
Integer(i64),
/// Floating-point value
Float(f64),
/// String value
String(String),
/// Boolean value
Boolean(bool),
/// Null value
Null,
// Unity-specific types
/// 2D vector (x, y)
Vector2(Vector2),
/// 3D vector (x, y, z)
Vector3(Vector3),
/// Color (r, g, b, a)
Color(Color),
/// Quaternion rotation (x, y, z, w)
Quaternion(Quaternion),
/// Reference to another object by file ID
FileRef(FileRef),
/// Reference to an external asset by GUID
ExternalRef(ExternalRef),
// Collections
/// Array of values
Array(Vec<PropertyValue>),
/// Nested object with properties
Object(IndexMap<String, PropertyValue>),
}
impl PropertyValue {
/// Try to get this value as an integer
pub fn as_i64(&self) -> Option<i64> {
match self {
PropertyValue::Integer(v) => Some(*v),
_ => None,
}
}
/// Try to get this value as a float
pub fn as_f64(&self) -> Option<f64> {
match self {
PropertyValue::Float(v) => Some(*v),
PropertyValue::Integer(v) => Some(*v as f64),
_ => None,
}
}
/// Try to get this value as a string reference
pub fn as_str(&self) -> Option<&str> {
match self {
PropertyValue::String(s) => Some(s.as_str()),
_ => None,
}
}
/// Try to get this value as a boolean
pub fn as_bool(&self) -> Option<bool> {
match self {
PropertyValue::Boolean(b) => Some(*b),
_ => None,
}
}
/// Try to get this value as a Vector2
pub fn as_vector2(&self) -> Option<&Vector2> {
match self {
PropertyValue::Vector2(v) => Some(v),
_ => None,
}
}
/// Try to get this value as a Vector3
pub fn as_vector3(&self) -> Option<&Vector3> {
match self {
PropertyValue::Vector3(v) => Some(v),
_ => None,
}
}
/// Try to get this value as a Color
pub fn as_color(&self) -> Option<&Color> {
match self {
PropertyValue::Color(c) => Some(c),
_ => None,
}
}
/// Try to get this value as a Quaternion
pub fn as_quaternion(&self) -> Option<&Quaternion> {
match self {
PropertyValue::Quaternion(q) => Some(q),
_ => None,
}
}
/// Try to get this value as a FileRef
pub fn as_file_ref(&self) -> Option<&FileRef> {
match self {
PropertyValue::FileRef(r) => Some(r),
_ => None,
}
}
/// Try to get this value as an ExternalRef
pub fn as_external_ref(&self) -> Option<&ExternalRef> {
match self {
PropertyValue::ExternalRef(r) => Some(r),
_ => None,
}
}
/// Try to get this value as an array
pub fn as_array(&self) -> Option<&Vec<PropertyValue>> {
match self {
PropertyValue::Array(arr) => Some(arr),
_ => None,
}
}
/// Try to get this value as an object
pub fn as_object(&self) -> Option<&IndexMap<String, PropertyValue>> {
match self {
PropertyValue::Object(obj) => Some(obj),
_ => None,
}
}
/// Check if this value is null
pub fn is_null(&self) -> bool {
matches!(self, PropertyValue::Null)
}
/// Check if this value is an array
pub fn is_array(&self) -> bool {
matches!(self, PropertyValue::Array(_))
}
/// Check if this value is an object
pub fn is_object(&self) -> bool {
matches!(self, PropertyValue::Object(_))
}
/// Check if this value is a Vector3
pub fn is_vector3(&self) -> bool {
matches!(self, PropertyValue::Vector3(_))
}
/// Check if this value is a Color
pub fn is_color(&self) -> bool {
matches!(self, PropertyValue::Color(_))
}
/// Check if this value is a FileRef
pub fn is_file_ref(&self) -> bool {
matches!(self, PropertyValue::FileRef(_))
}
}
impl fmt::Display for PropertyValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PropertyValue::Integer(v) => write!(f, "{}", v),
PropertyValue::Float(v) => write!(f, "{}", v),
PropertyValue::String(s) => write!(f, "\"{}\"", s),
PropertyValue::Boolean(b) => write!(f, "{}", b),
PropertyValue::Null => write!(f, "null"),
PropertyValue::Vector2(v) => write!(f, "({}, {})", v.x, v.y),
PropertyValue::Vector3(v) => write!(f, "({}, {}, {})", v.x, v.y, v.z),
PropertyValue::Color(c) => write!(f, "rgba({}, {}, {}, {})", c.r, c.g, c.b, c.a),
PropertyValue::Quaternion(q) => write!(f, "({}, {}, {}, {})", q.x, q.y, q.z, q.w),
PropertyValue::FileRef(r) => write!(f, "{{fileID: {}}}", r.file_id),
PropertyValue::ExternalRef(r) => write!(f, "{{guid: {}, type: {}}}", r.guid, r.type_id),
PropertyValue::Array(arr) => {
write!(f, "[")?;
for (i, item) in arr.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", item)?;
}
write!(f, "]")
}
PropertyValue::Object(obj) => {
write!(f, "{{")?;
for (i, (k, v)) in obj.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}: {}", k, v)?;
}
write!(f, "}}")
}
}
}
}
/// Convert a serde_yaml::Value to a PropertyValue
///
/// This function recognizes Unity-specific patterns in YAML mappings:
/// - `{fileID: N}` → `PropertyValue::FileRef`
/// - `{x, y, z}` → `PropertyValue::Vector3`
/// - `{x, y}` → `PropertyValue::Vector2`
/// - `{r, g, b, a}` → `PropertyValue::Color`
/// - `{x, y, z, w}` → `PropertyValue::Quaternion`
/// - `{guid, type}` → `PropertyValue::ExternalRef`
pub fn convert_yaml_value(value: &serde_yaml::Value) -> crate::Result<PropertyValue> {
match value {
serde_yaml::Value::Null => Ok(PropertyValue::Null),
serde_yaml::Value::Bool(b) => Ok(PropertyValue::Boolean(*b)),
serde_yaml::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(PropertyValue::Integer(i))
} else if let Some(f) = n.as_f64() {
Ok(PropertyValue::Float(f))
} else {
Err(Error::invalid_format(format!(
"Unsupported number format: {}",
n
)))
}
}
serde_yaml::Value::String(s) => Ok(PropertyValue::String(s.clone())),
serde_yaml::Value::Sequence(seq) => {
let mut array = Vec::with_capacity(seq.len());
for item in seq {
array.push(convert_yaml_value(item)?);
}
Ok(PropertyValue::Array(array))
}
serde_yaml::Value::Mapping(map) => {
// Check for Unity-specific patterns
if let Some(unity_type) = try_convert_unity_type(map)? {
Ok(unity_type)
} else {
// Convert to generic object
let mut object = IndexMap::new();
for (k, v) in map {
if let Some(key) = k.as_str() {
object.insert(key.to_string(), convert_yaml_value(v)?);
}
}
Ok(PropertyValue::Object(object))
}
}
_ => Err(Error::invalid_format(format!(
"Unsupported YAML value type: {:?}",
value
))),
}
}
/// Try to convert a YAML mapping to a Unity-specific type
fn try_convert_unity_type(
map: &serde_yaml::Mapping,
) -> crate::Result<Option<PropertyValue>> {
// Helper to get a float from a mapping
let get_f32 = |key: &str| -> Option<f32> {
map.get(&serde_yaml::Value::String(key.to_string()))
.and_then(|v| v.as_f64())
.map(|f| f as f32)
};
// Helper to get an i64 from a mapping
let get_i64 = |key: &str| -> Option<i64> {
map.get(&serde_yaml::Value::String(key.to_string()))
.and_then(|v| v.as_i64())
};
// Helper to get a string from a mapping
let get_string = |key: &str| -> Option<String> {
map.get(&serde_yaml::Value::String(key.to_string()))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
};
// Check for {fileID: N} pattern
if map.len() == 1 && map.contains_key(&serde_yaml::Value::String("fileID".to_string())) {
if let Some(file_id) = get_i64("fileID") {
return Ok(Some(PropertyValue::FileRef(FileRef::new(
FileID::from_i64(file_id),
))));
}
}
// Check for {guid: ..., type: N} pattern
if map.len() == 2
&& map.contains_key(&serde_yaml::Value::String("guid".to_string()))
&& map.contains_key(&serde_yaml::Value::String("type".to_string()))
{
if let (Some(guid), Some(type_id)) = (get_string("guid"), get_i64("type")) {
return Ok(Some(PropertyValue::ExternalRef(ExternalRef::new(
guid,
type_id as i32,
))));
}
}
// Check for {r, g, b, a} pattern (Color)
if map.len() == 4
&& map.contains_key(&serde_yaml::Value::String("r".to_string()))
&& map.contains_key(&serde_yaml::Value::String("g".to_string()))
&& map.contains_key(&serde_yaml::Value::String("b".to_string()))
&& map.contains_key(&serde_yaml::Value::String("a".to_string()))
{
if let (Some(r), Some(g), Some(b), Some(a)) =
(get_f32("r"), get_f32("g"), get_f32("b"), get_f32("a"))
{
return Ok(Some(PropertyValue::Color(Color::new(r, g, b, a))));
}
}
// Check for {x, y, z, w} pattern (Quaternion)
if map.len() == 4
&& map.contains_key(&serde_yaml::Value::String("x".to_string()))
&& map.contains_key(&serde_yaml::Value::String("y".to_string()))
&& map.contains_key(&serde_yaml::Value::String("z".to_string()))
&& map.contains_key(&serde_yaml::Value::String("w".to_string()))
{
if let (Some(x), Some(y), Some(z), Some(w)) =
(get_f32("x"), get_f32("y"), get_f32("z"), get_f32("w"))
{
return Ok(Some(PropertyValue::Quaternion(Quaternion::new(
x, y, z, w,
))));
}
}
// Check for {x, y, z} pattern (Vector3)
if map.len() == 3
&& map.contains_key(&serde_yaml::Value::String("x".to_string()))
&& map.contains_key(&serde_yaml::Value::String("y".to_string()))
&& map.contains_key(&serde_yaml::Value::String("z".to_string()))
{
if let (Some(x), Some(y), Some(z)) = (get_f32("x"), get_f32("y"), get_f32("z")) {
return Ok(Some(PropertyValue::Vector3(Vector3::new(x, y, z))));
}
}
// Check for {x, y} pattern (Vector2)
if map.len() == 2
&& map.contains_key(&serde_yaml::Value::String("x".to_string()))
&& map.contains_key(&serde_yaml::Value::String("y".to_string()))
{
if let (Some(x), Some(y)) = (get_f32("x"), get_f32("y")) {
return Ok(Some(PropertyValue::Vector2(Vector2::new(x, y))));
}
}
// Not a Unity-specific type
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_yaml::Value;
#[test]
fn test_convert_primitives() {
assert_eq!(
convert_yaml_value(&Value::Null).unwrap(),
PropertyValue::Null
);
assert_eq!(
convert_yaml_value(&Value::Bool(true)).unwrap(),
PropertyValue::Boolean(true)
);
assert_eq!(
convert_yaml_value(&Value::Number(42.into())).unwrap(),
PropertyValue::Integer(42)
);
assert_eq!(
convert_yaml_value(&Value::String("test".to_string())).unwrap(),
PropertyValue::String("test".to_string())
);
}
#[test]
fn test_convert_vector3() {
let yaml = serde_yaml::from_str("{ x: 1.0, y: 2.0, z: 3.0 }").unwrap();
let result = convert_yaml_value(&yaml).unwrap();
if let PropertyValue::Vector3(v) = result {
assert_eq!(v.x, 1.0);
assert_eq!(v.y, 2.0);
assert_eq!(v.z, 3.0);
} else {
panic!("Expected Vector3, got {:?}", result);
}
}
#[test]
fn test_convert_vector2() {
let yaml = serde_yaml::from_str("{ x: 1.0, y: 2.0 }").unwrap();
let result = convert_yaml_value(&yaml).unwrap();
if let PropertyValue::Vector2(v) = result {
assert_eq!(v.x, 1.0);
assert_eq!(v.y, 2.0);
} else {
panic!("Expected Vector2, got {:?}", result);
}
}
#[test]
fn test_convert_color() {
let yaml = serde_yaml::from_str("{ r: 1.0, g: 0.5, b: 0.0, a: 1.0 }").unwrap();
let result = convert_yaml_value(&yaml).unwrap();
if let PropertyValue::Color(c) = result {
assert_eq!(c.r, 1.0);
assert_eq!(c.g, 0.5);
assert_eq!(c.b, 0.0);
assert_eq!(c.a, 1.0);
} else {
panic!("Expected Color, got {:?}", result);
}
}
#[test]
fn test_convert_quaternion() {
let yaml = serde_yaml::from_str("{ x: 0.0, y: 0.0, z: 0.0, w: 1.0 }").unwrap();
let result = convert_yaml_value(&yaml).unwrap();
if let PropertyValue::Quaternion(q) = result {
assert_eq!(q.x, 0.0);
assert_eq!(q.w, 1.0);
} else {
panic!("Expected Quaternion, got {:?}", result);
}
}
#[test]
fn test_convert_file_ref() {
let yaml = serde_yaml::from_str("{ fileID: 12345 }").unwrap();
let result = convert_yaml_value(&yaml).unwrap();
if let PropertyValue::FileRef(r) = result {
assert_eq!(r.file_id.as_i64(), 12345);
} else {
panic!("Expected FileRef, got {:?}", result);
}
}
#[test]
fn test_convert_external_ref() {
let yaml = serde_yaml::from_str("{ guid: abc123, type: 2 }").unwrap();
let result = convert_yaml_value(&yaml).unwrap();
if let PropertyValue::ExternalRef(r) = result {
assert_eq!(r.guid, "abc123");
assert_eq!(r.type_id, 2);
} else {
panic!("Expected ExternalRef, got {:?}", result);
}
}
#[test]
fn test_convert_array() {
let yaml = serde_yaml::from_str("[1, 2, 3]").unwrap();
let result = convert_yaml_value(&yaml).unwrap();
if let PropertyValue::Array(arr) = result {
assert_eq!(arr.len(), 3);
assert_eq!(arr[0].as_i64(), Some(1));
assert_eq!(arr[1].as_i64(), Some(2));
assert_eq!(arr[2].as_i64(), Some(3));
} else {
panic!("Expected Array, got {:?}", result);
}
}
#[test]
fn test_convert_object() {
let yaml = serde_yaml::from_str("{ name: Test, value: 42 }").unwrap();
let result = convert_yaml_value(&yaml).unwrap();
if let PropertyValue::Object(obj) = result {
assert_eq!(obj.get("name").and_then(|v| v.as_str()), Some("Test"));
assert_eq!(obj.get("value").and_then(|v| v.as_i64()), Some(42));
} else {
panic!("Expected Object, got {:?}", result);
}
}
#[test]
fn test_accessors() {
let val = PropertyValue::Integer(42);
assert_eq!(val.as_i64(), Some(42));
assert_eq!(val.as_f64(), Some(42.0));
assert_eq!(val.as_str(), None);
let val = PropertyValue::String("test".to_string());
assert_eq!(val.as_str(), Some("test"));
assert_eq!(val.as_i64(), None);
}
#[test]
fn test_type_checks() {
assert!(PropertyValue::Null.is_null());
assert!(PropertyValue::Array(vec![]).is_array());
assert!(PropertyValue::Vector3(Vector3::zero()).is_vector3());
assert!(PropertyValue::Color(Color::white()).is_color());
}
#[test]
fn test_display() {
assert_eq!(format!("{}", PropertyValue::Integer(42)), "42");
assert_eq!(format!("{}", PropertyValue::String("test".to_string())), "\"test\"");
assert_eq!(format!("{}", PropertyValue::Vector3(Vector3::new(1.0, 2.0, 3.0))), "(1, 2, 3)");
}
}

163
src/types/component.rs Normal file
View File

@@ -0,0 +1,163 @@
//! Component trait and generic component wrapper
use crate::model::UnityDocument;
use crate::types::FileRef;
/// A trait for Unity components
///
/// Components are attached to GameObjects and provide functionality.
pub trait Component {
/// Get the GameObject this component is attached to
fn game_object(&self) -> Option<FileRef>;
/// Check if this component is enabled
fn is_enabled(&self) -> bool {
true // Default implementation
}
/// Get the underlying UnityDocument
fn document(&self) -> &UnityDocument;
}
/// A generic component wrapper that works with any component type
///
/// # Examples
///
/// ```no_run
/// use cursebreaker_parser::{UnityFile, types::{Component, GenericComponent}};
///
/// let file = UnityFile::from_path("Scene.unity")?;
/// for doc in &file.documents {
/// if !doc.is_game_object() {
/// if let Some(comp) = GenericComponent::new(doc) {
/// println!("Component: {}", doc.class_name);
/// if let Some(go_ref) = comp.game_object() {
/// println!(" Attached to: {}", go_ref.file_id);
/// }
/// }
/// }
/// }
/// # Ok::<(), cursebreaker_parser::Error>(())
/// ```
#[derive(Debug)]
pub struct GenericComponent<'a> {
document: &'a UnityDocument,
}
impl<'a> GenericComponent<'a> {
/// Create a GenericComponent wrapper from a UnityDocument
///
/// Returns None if the document is a GameObject (GameObjects are not components).
pub fn new(document: &'a UnityDocument) -> Option<Self> {
if !document.is_game_object() {
Some(Self { document })
} else {
None
}
}
/// Get the class name of this component
pub fn class_name(&self) -> &str {
&self.document.class_name
}
}
impl<'a> Component for GenericComponent<'a> {
fn game_object(&self) -> Option<FileRef> {
// Look for m_GameObject property which is common to all components
self.document
.get(&self.document.class_name)
.and_then(|obj| obj.as_object())
.and_then(|props| props.get("m_GameObject"))
.and_then(|v| v.as_file_ref())
.copied()
}
fn is_enabled(&self) -> bool {
// Look for m_Enabled property
self.document
.get(&self.document.class_name)
.and_then(|obj| obj.as_object())
.and_then(|props| props.get("m_Enabled"))
.and_then(|v| v.as_bool())
.unwrap_or(true) // Default to enabled
}
fn document(&self) -> &UnityDocument {
self.document
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::property::PropertyValue;
use crate::types::FileID;
use indexmap::IndexMap;
fn create_test_component() -> UnityDocument {
let mut properties = IndexMap::new();
let mut comp_props = IndexMap::new();
comp_props.insert(
"m_GameObject".to_string(),
PropertyValue::FileRef(crate::types::FileRef::new(FileID::from_i64(67890))),
);
comp_props.insert("m_Enabled".to_string(), PropertyValue::Boolean(true));
properties.insert("Transform".to_string(), PropertyValue::Object(comp_props));
UnityDocument {
type_id: 4,
file_id: FileID::from_i64(12345),
class_name: "Transform".to_string(),
properties,
}
}
#[test]
fn test_component_creation() {
let doc = create_test_component();
let comp = GenericComponent::new(&doc);
assert!(comp.is_some());
}
#[test]
fn test_component_game_object_ref() {
let doc = create_test_component();
let comp = GenericComponent::new(&doc).unwrap();
let go_ref = comp.game_object();
assert!(go_ref.is_some());
assert_eq!(go_ref.unwrap().file_id.as_i64(), 67890);
}
#[test]
fn test_component_is_enabled() {
let doc = create_test_component();
let comp = GenericComponent::new(&doc).unwrap();
assert!(comp.is_enabled());
}
#[test]
fn test_component_class_name() {
let doc = create_test_component();
let comp = GenericComponent::new(&doc).unwrap();
assert_eq!(comp.class_name(), "Transform");
}
#[test]
fn test_game_object_is_not_component() {
let mut properties = IndexMap::new();
properties.insert("GameObject".to_string(), PropertyValue::Object(IndexMap::new()));
let doc = UnityDocument {
type_id: 1,
file_id: FileID::from_i64(12345),
class_name: "GameObject".to_string(),
properties,
};
let comp = GenericComponent::new(&doc);
assert!(comp.is_none());
}
}

180
src/types/game_object.rs Normal file
View File

@@ -0,0 +1,180 @@
//! GameObject wrapper for ergonomic access to GameObject properties
use crate::model::UnityDocument;
use crate::types::{FileID, FileRef};
/// A wrapper around a UnityDocument that represents a GameObject
///
/// Provides convenient access to common GameObject properties.
///
/// # Examples
///
/// ```no_run
/// use cursebreaker_parser::{UnityFile, types::GameObject};
///
/// let file = UnityFile::from_path("Scene.unity")?;
/// for doc in &file.documents {
/// if let Some(go) = GameObject::new(doc) {
/// println!("GameObject: {}", go.name().unwrap_or("Unnamed"));
/// println!(" Active: {}", go.is_active());
/// println!(" Components: {}", go.components().len());
/// }
/// }
/// # Ok::<(), cursebreaker_parser::Error>(())
/// ```
#[derive(Debug)]
pub struct GameObject<'a> {
document: &'a UnityDocument,
}
impl<'a> GameObject<'a> {
/// Create a GameObject wrapper from a UnityDocument
///
/// Returns None if the document is not a GameObject.
pub fn new(document: &'a UnityDocument) -> Option<Self> {
if document.is_game_object() {
Some(Self { document })
} else {
None
}
}
/// Get the GameObject's name
pub fn name(&self) -> Option<&str> {
self.document
.get("GameObject")
.and_then(|obj| obj.as_object())
.and_then(|props| props.get("m_Name"))
.and_then(|v| v.as_str())
}
/// Check if the GameObject is active
pub fn is_active(&self) -> bool {
self.document
.get("GameObject")
.and_then(|obj| obj.as_object())
.and_then(|props| props.get("m_IsActive"))
.and_then(|v| v.as_bool())
.unwrap_or(true) // Default to true if not specified
}
/// Get the GameObject's layer
pub fn layer(&self) -> Option<i64> {
self.document
.get("GameObject")
.and_then(|obj| obj.as_object())
.and_then(|props| props.get("m_Layer"))
.and_then(|v| v.as_i64())
}
/// Get the GameObject's tag as a tag ID
pub fn tag(&self) -> Option<i64> {
self.document
.get("GameObject")
.and_then(|obj| obj.as_object())
.and_then(|props| props.get("m_TagString"))
.and_then(|v| v.as_i64())
}
/// Get the list of component references attached to this GameObject
pub fn components(&self) -> Vec<FileRef> {
self.document
.get("GameObject")
.and_then(|obj| obj.as_object())
.and_then(|props| props.get("m_Component"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|item| {
item.as_object().and_then(|obj| {
obj.get("component")
.and_then(|v| v.as_file_ref())
.copied()
})
})
.collect()
})
.unwrap_or_default()
}
/// Get the file ID of this GameObject
pub fn file_id(&self) -> FileID {
self.document.file_id
}
/// Get the underlying UnityDocument
pub fn document(&self) -> &'a UnityDocument {
self.document
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::property::PropertyValue;
use indexmap::IndexMap;
fn create_test_game_object() -> UnityDocument {
let mut properties = IndexMap::new();
let mut go_props = IndexMap::new();
go_props.insert("m_Name".to_string(), PropertyValue::String("TestObject".to_string()));
go_props.insert("m_IsActive".to_string(), PropertyValue::Boolean(true));
go_props.insert("m_Layer".to_string(), PropertyValue::Integer(0));
go_props.insert("m_Component".to_string(), PropertyValue::Array(vec![]));
properties.insert("GameObject".to_string(), PropertyValue::Object(go_props));
UnityDocument {
type_id: 1,
file_id: FileID::from_i64(12345),
class_name: "GameObject".to_string(),
properties,
}
}
#[test]
fn test_game_object_creation() {
let doc = create_test_game_object();
let go = GameObject::new(&doc);
assert!(go.is_some());
}
#[test]
fn test_game_object_name() {
let doc = create_test_game_object();
let go = GameObject::new(&doc).unwrap();
assert_eq!(go.name(), Some("TestObject"));
}
#[test]
fn test_game_object_is_active() {
let doc = create_test_game_object();
let go = GameObject::new(&doc).unwrap();
assert!(go.is_active());
}
#[test]
fn test_game_object_layer() {
let doc = create_test_game_object();
let go = GameObject::new(&doc).unwrap();
assert_eq!(go.layer(), Some(0));
}
#[test]
fn test_game_object_file_id() {
let doc = create_test_game_object();
let go = GameObject::new(&doc).unwrap();
assert_eq!(go.file_id().as_i64(), 12345);
}
#[test]
fn test_non_game_object() {
let mut doc = create_test_game_object();
doc.type_id = 4; // Transform type ID
doc.class_name = "Transform".to_string();
let go = GameObject::new(&doc);
assert!(go.is_none());
}
}

131
src/types/ids.rs Normal file
View File

@@ -0,0 +1,131 @@
//! Unity ID types
use std::fmt;
/// A Unity file ID, used to reference Unity objects within a file
///
/// # Examples
///
/// ```
/// use cursebreaker_parser::FileID;
///
/// let file_id = FileID::from_i64(1866116814460599870);
/// assert_eq!(file_id.as_i64(), 1866116814460599870);
/// ```
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct FileID(i64);
impl FileID {
/// Create a FileID from an i64
pub fn from_i64(id: i64) -> Self {
Self(id)
}
/// Get the underlying i64 value
pub fn as_i64(&self) -> i64 {
self.0
}
}
impl fmt::Display for FileID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<i64> for FileID {
fn from(id: i64) -> Self {
Self::from_i64(id)
}
}
impl From<FileID> for i64 {
fn from(file_id: FileID) -> Self {
file_id.as_i64()
}
}
/// A local ID for objects within a Unity file
///
/// This is currently an alias for FileID but may have different
/// semantics in future versions.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct LocalID(i64);
impl LocalID {
/// Create a LocalID from an i64
pub fn from_i64(id: i64) -> Self {
Self(id)
}
/// Get the underlying i64 value
pub fn as_i64(&self) -> i64 {
self.0
}
}
impl fmt::Display for LocalID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<i64> for LocalID {
fn from(id: i64) -> Self {
Self::from_i64(id)
}
}
impl From<LocalID> for i64 {
fn from(local_id: LocalID) -> Self {
local_id.as_i64()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_id_creation() {
let file_id = FileID::from_i64(12345);
assert_eq!(file_id.as_i64(), 12345);
}
#[test]
fn test_file_id_display() {
let file_id = FileID::from_i64(1866116814460599870);
assert_eq!(format!("{}", file_id), "1866116814460599870");
}
#[test]
fn test_file_id_equality() {
let id1 = FileID::from_i64(12345);
let id2 = FileID::from_i64(12345);
let id3 = FileID::from_i64(67890);
assert_eq!(id1, id2);
assert_ne!(id1, id3);
}
#[test]
fn test_file_id_conversion() {
let id: FileID = 12345.into();
assert_eq!(id.as_i64(), 12345);
let value: i64 = id.into();
assert_eq!(value, 12345);
}
#[test]
fn test_local_id_creation() {
let local_id = LocalID::from_i64(12345);
assert_eq!(local_id.as_i64(), 12345);
}
#[test]
fn test_local_id_display() {
let local_id = LocalID::from_i64(67890);
assert_eq!(format!("{}", local_id), "67890");
}
}

17
src/types/mod.rs Normal file
View File

@@ -0,0 +1,17 @@
//! Unity-specific types and wrappers
//!
//! This module provides type-safe representations of Unity types,
//! including IDs, value types (Vector3, Color, etc.), and high-level
//! wrappers for GameObjects and Components.
mod component;
mod game_object;
mod ids;
mod transform;
mod values;
pub use component::{Component, GenericComponent};
pub use game_object::GameObject;
pub use ids::{FileID, LocalID};
pub use transform::{RectTransform, Transform};
pub use values::{Color, ExternalRef, FileRef, Quaternion, Vector2, Vector3};

398
src/types/transform.rs Normal file
View File

@@ -0,0 +1,398 @@
//! Transform and RectTransform component wrappers
use crate::model::UnityDocument;
use crate::types::{Component, FileRef, Quaternion, Vector2, Vector3};
/// A wrapper around a UnityDocument that represents a Transform component
///
/// Provides convenient access to Transform properties like position, rotation, and scale.
///
/// # Examples
///
/// ```no_run
/// use cursebreaker_parser::{UnityFile, types::Transform};
///
/// let file = UnityFile::from_path("Scene.unity")?;
/// for doc in &file.documents {
/// if let Some(transform) = Transform::new(doc) {
/// if let Some(pos) = transform.local_position() {
/// println!("Position: ({}, {}, {})", pos.x, pos.y, pos.z);
/// }
/// }
/// }
/// # Ok::<(), cursebreaker_parser::Error>(())
/// ```
#[derive(Debug)]
pub struct Transform<'a> {
document: &'a UnityDocument,
}
impl<'a> Transform<'a> {
/// Create a Transform wrapper from a UnityDocument
///
/// Returns None if the document is not a Transform or RectTransform.
pub fn new(document: &'a UnityDocument) -> Option<Self> {
if document.class_name == "Transform" || document.class_name == "RectTransform" {
Some(Self { document })
} else {
None
}
}
/// Get the local position of this transform
pub fn local_position(&self) -> Option<&Vector3> {
self.get_transform_props()
.and_then(|props| props.get("m_LocalPosition"))
.and_then(|v| v.as_vector3())
}
/// Get the local rotation of this transform
pub fn local_rotation(&self) -> Option<&Quaternion> {
self.get_transform_props()
.and_then(|props| props.get("m_LocalRotation"))
.and_then(|v| v.as_quaternion())
}
/// Get the local scale of this transform
pub fn local_scale(&self) -> Option<&Vector3> {
self.get_transform_props()
.and_then(|props| props.get("m_LocalScale"))
.and_then(|v| v.as_vector3())
}
/// Get the parent transform reference
pub fn parent(&self) -> Option<FileRef> {
self.get_transform_props()
.and_then(|props| props.get("m_Father"))
.and_then(|v| v.as_file_ref())
.copied()
}
/// Get the list of child transform references
pub fn children(&self) -> Vec<FileRef> {
self.get_transform_props()
.and_then(|props| props.get("m_Children"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|item| item.as_file_ref())
.copied()
.collect()
})
.unwrap_or_default()
}
/// Helper to get the transform properties object
fn get_transform_props(&self) -> Option<&indexmap::IndexMap<String, crate::property::PropertyValue>> {
self.document
.get(&self.document.class_name)
.and_then(|v| v.as_object())
}
}
impl<'a> Component for Transform<'a> {
fn game_object(&self) -> Option<FileRef> {
self.get_transform_props()
.and_then(|props| props.get("m_GameObject"))
.and_then(|v| v.as_file_ref())
.copied()
}
fn is_enabled(&self) -> bool {
true // Transforms are always enabled
}
fn document(&self) -> &UnityDocument {
self.document
}
}
/// A wrapper around a UnityDocument that represents a RectTransform component
///
/// RectTransform is used for UI elements and extends Transform with additional properties.
///
/// # Examples
///
/// ```no_run
/// use cursebreaker_parser::{UnityFile, types::RectTransform};
///
/// let file = UnityFile::from_path("Canvas.prefab")?;
/// for doc in &file.documents {
/// if let Some(rect_transform) = RectTransform::new(doc) {
/// if let Some(anchor_min) = rect_transform.anchor_min() {
/// println!("Anchor Min: ({}, {})", anchor_min.x, anchor_min.y);
/// }
/// }
/// }
/// # Ok::<(), cursebreaker_parser::Error>(())
/// ```
#[derive(Debug)]
pub struct RectTransform<'a> {
transform: Transform<'a>,
}
impl<'a> RectTransform<'a> {
/// Create a RectTransform wrapper from a UnityDocument
///
/// Returns None if the document is not a RectTransform.
pub fn new(document: &'a UnityDocument) -> Option<Self> {
if document.class_name == "RectTransform" {
Some(Self {
transform: Transform { document },
})
} else {
None
}
}
/// Get the anchor min (bottom-left anchor)
pub fn anchor_min(&self) -> Option<&Vector2> {
self.get_rect_props()
.and_then(|props| props.get("m_AnchorMin"))
.and_then(|v| v.as_vector2())
}
/// Get the anchor max (top-right anchor)
pub fn anchor_max(&self) -> Option<&Vector2> {
self.get_rect_props()
.and_then(|props| props.get("m_AnchorMax"))
.and_then(|v| v.as_vector2())
}
/// Get the anchored position
pub fn anchored_position(&self) -> Option<&Vector2> {
self.get_rect_props()
.and_then(|props| props.get("m_AnchoredPosition"))
.and_then(|v| v.as_vector2())
}
/// Get the size delta
pub fn size_delta(&self) -> Option<&Vector2> {
self.get_rect_props()
.and_then(|props| props.get("m_SizeDelta"))
.and_then(|v| v.as_vector2())
}
/// Get the pivot point
pub fn pivot(&self) -> Option<&Vector2> {
self.get_rect_props()
.and_then(|props| props.get("m_Pivot"))
.and_then(|v| v.as_vector2())
}
/// Get the local position (from Transform)
pub fn local_position(&self) -> Option<&Vector3> {
self.transform.local_position()
}
/// Get the local rotation (from Transform)
pub fn local_rotation(&self) -> Option<&Quaternion> {
self.transform.local_rotation()
}
/// Get the local scale (from Transform)
pub fn local_scale(&self) -> Option<&Vector3> {
self.transform.local_scale()
}
/// Get the parent transform reference (from Transform)
pub fn parent(&self) -> Option<FileRef> {
self.transform.parent()
}
/// Get the list of child transform references (from Transform)
pub fn children(&self) -> Vec<FileRef> {
self.transform.children()
}
/// Helper to get the RectTransform properties object
fn get_rect_props(&self) -> Option<&indexmap::IndexMap<String, crate::property::PropertyValue>> {
self.transform.get_transform_props()
}
}
impl<'a> Component for RectTransform<'a> {
fn game_object(&self) -> Option<FileRef> {
self.transform.game_object()
}
fn is_enabled(&self) -> bool {
self.transform.is_enabled()
}
fn document(&self) -> &UnityDocument {
self.transform.document()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::property::PropertyValue;
use crate::types::FileID;
use indexmap::IndexMap;
fn create_test_transform() -> UnityDocument {
let mut properties = IndexMap::new();
let mut transform_props = IndexMap::new();
transform_props.insert(
"m_LocalPosition".to_string(),
PropertyValue::Vector3(Vector3::new(1.0, 2.0, 3.0)),
);
transform_props.insert(
"m_LocalRotation".to_string(),
PropertyValue::Quaternion(Quaternion::identity()),
);
transform_props.insert(
"m_LocalScale".to_string(),
PropertyValue::Vector3(Vector3::one()),
);
transform_props.insert(
"m_GameObject".to_string(),
PropertyValue::FileRef(crate::types::FileRef::new(FileID::from_i64(67890))),
);
transform_props.insert("m_Children".to_string(), PropertyValue::Array(vec![]));
properties.insert("Transform".to_string(), PropertyValue::Object(transform_props));
UnityDocument {
type_id: 4,
file_id: FileID::from_i64(12345),
class_name: "Transform".to_string(),
properties,
}
}
fn create_test_rect_transform() -> UnityDocument {
let mut properties = IndexMap::new();
let mut rect_props = IndexMap::new();
rect_props.insert(
"m_LocalPosition".to_string(),
PropertyValue::Vector3(Vector3::zero()),
);
rect_props.insert(
"m_LocalRotation".to_string(),
PropertyValue::Quaternion(Quaternion::identity()),
);
rect_props.insert(
"m_LocalScale".to_string(),
PropertyValue::Vector3(Vector3::one()),
);
rect_props.insert(
"m_AnchorMin".to_string(),
PropertyValue::Vector2(Vector2::zero()),
);
rect_props.insert(
"m_AnchorMax".to_string(),
PropertyValue::Vector2(Vector2::one()),
);
rect_props.insert(
"m_AnchoredPosition".to_string(),
PropertyValue::Vector2(Vector2::zero()),
);
rect_props.insert(
"m_SizeDelta".to_string(),
PropertyValue::Vector2(Vector2::new(100.0, 50.0)),
);
rect_props.insert(
"m_Pivot".to_string(),
PropertyValue::Vector2(Vector2::new(0.5, 0.5)),
);
properties.insert("RectTransform".to_string(), PropertyValue::Object(rect_props));
UnityDocument {
type_id: 224,
file_id: FileID::from_i64(12345),
class_name: "RectTransform".to_string(),
properties,
}
}
#[test]
fn test_transform_creation() {
let doc = create_test_transform();
let transform = Transform::new(&doc);
assert!(transform.is_some());
}
#[test]
fn test_transform_local_position() {
let doc = create_test_transform();
let transform = Transform::new(&doc).unwrap();
let pos = transform.local_position().unwrap();
assert_eq!(pos.x, 1.0);
assert_eq!(pos.y, 2.0);
assert_eq!(pos.z, 3.0);
}
#[test]
fn test_transform_local_rotation() {
let doc = create_test_transform();
let transform = Transform::new(&doc).unwrap();
let rot = transform.local_rotation().unwrap();
assert_eq!(rot.w, 1.0);
}
#[test]
fn test_transform_local_scale() {
let doc = create_test_transform();
let transform = Transform::new(&doc).unwrap();
let scale = transform.local_scale().unwrap();
assert_eq!(scale, &Vector3::one());
}
#[test]
fn test_rect_transform_creation() {
let doc = create_test_rect_transform();
let rect_transform = RectTransform::new(&doc);
assert!(rect_transform.is_some());
}
#[test]
fn test_rect_transform_anchor_min() {
let doc = create_test_rect_transform();
let rect_transform = RectTransform::new(&doc).unwrap();
let anchor_min = rect_transform.anchor_min().unwrap();
assert_eq!(anchor_min, &Vector2::zero());
}
#[test]
fn test_rect_transform_anchor_max() {
let doc = create_test_rect_transform();
let rect_transform = RectTransform::new(&doc).unwrap();
let anchor_max = rect_transform.anchor_max().unwrap();
assert_eq!(anchor_max, &Vector2::one());
}
#[test]
fn test_rect_transform_size_delta() {
let doc = create_test_rect_transform();
let rect_transform = RectTransform::new(&doc).unwrap();
let size_delta = rect_transform.size_delta().unwrap();
assert_eq!(size_delta.x, 100.0);
assert_eq!(size_delta.y, 50.0);
}
#[test]
fn test_rect_transform_pivot() {
let doc = create_test_rect_transform();
let rect_transform = RectTransform::new(&doc).unwrap();
let pivot = rect_transform.pivot().unwrap();
assert_eq!(pivot.x, 0.5);
assert_eq!(pivot.y, 0.5);
}
#[test]
fn test_rect_transform_inherits_from_transform() {
let doc = create_test_rect_transform();
let rect_transform = RectTransform::new(&doc).unwrap();
// Test inherited methods
assert!(rect_transform.local_position().is_some());
assert!(rect_transform.local_rotation().is_some());
assert!(rect_transform.local_scale().is_some());
}
}

303
src/types/values.rs Normal file
View File

@@ -0,0 +1,303 @@
//! Unity-specific value types
use super::FileID;
/// A 2D vector used in Unity
///
/// # Examples
///
/// ```
/// use cursebreaker_parser::Vector2;
///
/// let v = Vector2::new(1.0, 2.0);
/// assert_eq!(v.x, 1.0);
/// assert_eq!(v.y, 2.0);
/// ```
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Vector2 {
pub x: f32,
pub y: f32,
}
impl Vector2 {
/// Create a new Vector2
pub fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
/// Create a zero vector (0, 0)
pub fn zero() -> Self {
Self::new(0.0, 0.0)
}
/// Create a one vector (1, 1)
pub fn one() -> Self {
Self::new(1.0, 1.0)
}
}
/// A 3D vector used in Unity for positions, scales, etc.
///
/// # Examples
///
/// ```
/// use cursebreaker_parser::Vector3;
///
/// let v = Vector3::new(1.0, 2.0, 3.0);
/// assert_eq!(v.x, 1.0);
/// assert_eq!(v.y, 2.0);
/// assert_eq!(v.z, 3.0);
/// ```
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Vector3 {
pub x: f32,
pub y: f32,
pub z: f32,
}
impl Vector3 {
/// Create a new Vector3
pub fn new(x: f32, y: f32, z: f32) -> Self {
Self { x, y, z }
}
/// Create a zero vector (0, 0, 0)
pub fn zero() -> Self {
Self::new(0.0, 0.0, 0.0)
}
/// Create a one vector (1, 1, 1)
pub fn one() -> Self {
Self::new(1.0, 1.0, 1.0)
}
/// Create an up vector (0, 1, 0)
pub fn up() -> Self {
Self::new(0.0, 1.0, 0.0)
}
/// Create a forward vector (0, 0, 1)
pub fn forward() -> Self {
Self::new(0.0, 0.0, 1.0)
}
/// Create a right vector (1, 0, 0)
pub fn right() -> Self {
Self::new(1.0, 0.0, 0.0)
}
}
/// A color with RGBA components
///
/// # Examples
///
/// ```
/// use cursebreaker_parser::Color;
///
/// let c = Color::new(1.0, 0.5, 0.0, 1.0);
/// assert_eq!(c.r, 1.0);
/// assert_eq!(c.a, 1.0);
/// ```
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Color {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
impl Color {
/// Create a new Color
pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
Self { r, g, b, a }
}
/// Create a white color (1, 1, 1, 1)
pub fn white() -> Self {
Self::new(1.0, 1.0, 1.0, 1.0)
}
/// Create a black color (0, 0, 0, 1)
pub fn black() -> Self {
Self::new(0.0, 0.0, 0.0, 1.0)
}
/// Create a transparent color (0, 0, 0, 0)
pub fn clear() -> Self {
Self::new(0.0, 0.0, 0.0, 0.0)
}
/// Create a red color (1, 0, 0, 1)
pub fn red() -> Self {
Self::new(1.0, 0.0, 0.0, 1.0)
}
/// Create a green color (0, 1, 0, 1)
pub fn green() -> Self {
Self::new(0.0, 1.0, 0.0, 1.0)
}
/// Create a blue color (0, 0, 1, 1)
pub fn blue() -> Self {
Self::new(0.0, 0.0, 1.0, 1.0)
}
}
/// A quaternion used for rotations in Unity
///
/// # Examples
///
/// ```
/// use cursebreaker_parser::Quaternion;
///
/// let q = Quaternion::identity();
/// assert_eq!(q.w, 1.0);
/// ```
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Quaternion {
pub x: f32,
pub y: f32,
pub z: f32,
pub w: f32,
}
impl Quaternion {
/// Create a new Quaternion
pub fn new(x: f32, y: f32, z: f32, w: f32) -> Self {
Self { x, y, z, w }
}
/// Create an identity quaternion (0, 0, 0, 1)
pub fn identity() -> Self {
Self::new(0.0, 0.0, 0.0, 1.0)
}
}
/// A reference to another Unity object by file ID
///
/// # Examples
///
/// ```
/// use cursebreaker_parser::{FileRef, FileID};
///
/// let file_ref = FileRef::new(FileID::from_i64(12345));
/// assert_eq!(file_ref.file_id.as_i64(), 12345);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FileRef {
pub file_id: FileID,
}
impl FileRef {
/// Create a new FileRef
pub fn new(file_id: FileID) -> Self {
Self { file_id }
}
}
/// A reference to an external Unity asset by GUID
///
/// # Examples
///
/// ```
/// use cursebreaker_parser::ExternalRef;
///
/// let ext_ref = ExternalRef::new("abc123".to_string(), 2);
/// assert_eq!(ext_ref.guid, "abc123");
/// assert_eq!(ext_ref.type_id, 2);
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ExternalRef {
pub guid: String,
pub type_id: i32,
}
impl ExternalRef {
/// Create a new ExternalRef
pub fn new(guid: String, type_id: i32) -> Self {
Self { guid, type_id }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vector2_creation() {
let v = Vector2::new(1.0, 2.0);
assert_eq!(v.x, 1.0);
assert_eq!(v.y, 2.0);
}
#[test]
fn test_vector2_zero() {
let v = Vector2::zero();
assert_eq!(v, Vector2::new(0.0, 0.0));
}
#[test]
fn test_vector3_creation() {
let v = Vector3::new(1.0, 2.0, 3.0);
assert_eq!(v.x, 1.0);
assert_eq!(v.y, 2.0);
assert_eq!(v.z, 3.0);
}
#[test]
fn test_vector3_zero() {
let v = Vector3::zero();
assert_eq!(v, Vector3::new(0.0, 0.0, 0.0));
}
#[test]
fn test_vector3_directions() {
assert_eq!(Vector3::up(), Vector3::new(0.0, 1.0, 0.0));
assert_eq!(Vector3::forward(), Vector3::new(0.0, 0.0, 1.0));
assert_eq!(Vector3::right(), Vector3::new(1.0, 0.0, 0.0));
}
#[test]
fn test_color_creation() {
let c = Color::new(1.0, 0.5, 0.0, 1.0);
assert_eq!(c.r, 1.0);
assert_eq!(c.g, 0.5);
assert_eq!(c.b, 0.0);
assert_eq!(c.a, 1.0);
}
#[test]
fn test_color_presets() {
assert_eq!(Color::white(), Color::new(1.0, 1.0, 1.0, 1.0));
assert_eq!(Color::black(), Color::new(0.0, 0.0, 0.0, 1.0));
assert_eq!(Color::red(), Color::new(1.0, 0.0, 0.0, 1.0));
assert_eq!(Color::green(), Color::new(0.0, 1.0, 0.0, 1.0));
assert_eq!(Color::blue(), Color::new(0.0, 0.0, 1.0, 1.0));
}
#[test]
fn test_quaternion_creation() {
let q = Quaternion::new(0.0, 0.0, 0.0, 1.0);
assert_eq!(q.x, 0.0);
assert_eq!(q.w, 1.0);
}
#[test]
fn test_quaternion_identity() {
let q = Quaternion::identity();
assert_eq!(q, Quaternion::new(0.0, 0.0, 0.0, 1.0));
}
#[test]
fn test_file_ref_creation() {
let file_ref = FileRef::new(FileID::from_i64(12345));
assert_eq!(file_ref.file_id.as_i64(), 12345);
}
#[test]
fn test_external_ref_creation() {
let ext_ref = ExternalRef::new("abc123".to_string(), 2);
assert_eq!(ext_ref.guid, "abc123");
assert_eq!(ext_ref.type_id, 2);
}
}

View File

@@ -25,8 +25,8 @@ fn test_parse_cardgrabber_prefab() {
// Verify the name property exists // Verify the name property exists
if let Some(go_props) = game_object.get("GameObject") { if let Some(go_props) = game_object.get("GameObject") {
if let Some(props) = go_props.as_mapping() { if let Some(props) = go_props.as_object() {
let has_name = props.keys().any(|k| k.as_str() == Some("m_Name")); let has_name = props.contains_key("m_Name");
assert!(has_name, "GameObject should have m_Name property"); assert!(has_name, "GameObject should have m_Name property");
} }
} }