Phase 2
This commit is contained in:
@@ -35,9 +35,9 @@ fn main() {
|
||||
println!("Found {} GameObjects:", game_objects.len());
|
||||
for go in game_objects {
|
||||
if let Some(go_props) = go.get("GameObject") {
|
||||
if let Some(props) = go_props.as_mapping() {
|
||||
if let Some(name) = props.get(&serde_yaml::Value::String("m_Name".to_string())) {
|
||||
println!(" - {}", name.as_str().unwrap_or("Unknown"));
|
||||
if let Some(props) = go_props.as_object() {
|
||||
if let Some(name) = props.get("m_Name").and_then(|v| v.as_str()) {
|
||||
println!(" - {}", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,14 @@ pub enum Error {
|
||||
/// Type conversion error
|
||||
#[error("Type conversion error: expected {expected}, found {found}")]
|
||||
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 {
|
||||
|
||||
@@ -19,8 +19,15 @@
|
||||
pub mod error;
|
||||
pub mod model;
|
||||
pub mod parser;
|
||||
pub mod property;
|
||||
pub mod types;
|
||||
|
||||
// Re-exports
|
||||
pub use error::{Error, Result};
|
||||
pub use model::{UnityDocument, UnityFile};
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::property::PropertyValue;
|
||||
use crate::types::{Color, FileID, Quaternion, Vector2, Vector3};
|
||||
use indexmap::IndexMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -27,7 +29,7 @@ impl UnityFile {
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
@@ -55,7 +57,7 @@ pub struct UnityDocument {
|
||||
pub type_id: u32,
|
||||
|
||||
/// File ID (from &ID anchor)
|
||||
pub file_id: i64,
|
||||
pub file_id: FileID,
|
||||
|
||||
/// Class name (e.g., "GameObject", "Transform", "RectTransform")
|
||||
pub class_name: String,
|
||||
@@ -66,7 +68,7 @@ pub struct UnityDocument {
|
||||
|
||||
impl 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 {
|
||||
type_id,
|
||||
file_id,
|
||||
@@ -76,10 +78,67 @@ impl UnityDocument {
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// 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
|
||||
pub fn is_game_object(&self) -> bool {
|
||||
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)
|
||||
pub type PropertyMap = IndexMap<String, serde_yaml::Value>;
|
||||
/// Property map type (ordered map of string keys to typed property values)
|
||||
pub type PropertyMap = IndexMap<String, PropertyValue>;
|
||||
|
||||
@@ -6,6 +6,7 @@ mod yaml;
|
||||
pub use unity_tag::{UnityTag, parse_unity_tag};
|
||||
pub use yaml::split_yaml_documents;
|
||||
|
||||
use crate::property::convert_yaml_value;
|
||||
use crate::{Error, Result, UnityDocument, UnityFile};
|
||||
use std::path::Path;
|
||||
|
||||
@@ -74,10 +75,14 @@ fn parse_document(raw_doc: &str) -> Result<Option<UnityDocument>> {
|
||||
} else {
|
||||
match serde_yaml::from_str::<serde_yaml::Value>(yaml_content) {
|
||||
Ok(serde_yaml::Value::Mapping(map)) => {
|
||||
// Convert to IndexMap
|
||||
// Convert to IndexMap with PropertyValue
|
||||
map.into_iter()
|
||||
.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()
|
||||
}
|
||||
@@ -95,7 +100,7 @@ fn parse_document(raw_doc: &str) -> Result<Option<UnityDocument>> {
|
||||
|
||||
Ok(Some(UnityDocument {
|
||||
type_id: tag.type_id,
|
||||
file_id: tag.file_id,
|
||||
file_id: crate::types::FileID::from_i64(tag.file_id),
|
||||
class_name,
|
||||
properties,
|
||||
}))
|
||||
|
||||
531
src/property/mod.rs
Normal file
531
src/property/mod.rs
Normal 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
163
src/types/component.rs
Normal 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
180
src/types/game_object.rs
Normal 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
131
src/types/ids.rs
Normal 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
17
src/types/mod.rs
Normal 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
398
src/types/transform.rs
Normal 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
303
src/types/values.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,8 @@ fn test_parse_cardgrabber_prefab() {
|
||||
|
||||
// Verify the name property exists
|
||||
if let Some(go_props) = game_object.get("GameObject") {
|
||||
if let Some(props) = go_props.as_mapping() {
|
||||
let has_name = props.keys().any(|k| k.as_str() == Some("m_Name"));
|
||||
if let Some(props) = go_props.as_object() {
|
||||
let has_name = props.contains_key("m_Name");
|
||||
assert!(has_name, "GameObject should have m_Name property");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user