unity_asset_yaml/
serde_unity_loader.rs

1//! Unity YAML loader based on serde_yaml
2//!
3//! This module provides a more robust Unity YAML loader that uses the mature
4//! serde_yaml library as its foundation and adds Unity-specific extensions.
5
6use crate::Result;
7use indexmap::IndexMap;
8use serde::Deserialize;
9use serde_yaml::Value;
10use std::io::Read;
11use unity_asset_core::{UnityAssetError, UnityClass, UnityValue};
12
13#[cfg(feature = "async")]
14use tokio::io::{AsyncRead, AsyncReadExt};
15
16#[derive(Debug, Clone)]
17pub struct SerdeUnityWarning {
18    pub doc_index: usize,
19    pub error: String,
20}
21
22impl std::fmt::Display for SerdeUnityWarning {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        write!(
25            f,
26            "Failed to convert document {}: {}",
27            self.doc_index, self.error
28        )
29    }
30}
31
32/// Unity YAML loader based on serde_yaml
33pub struct SerdeUnityLoader;
34
35impl SerdeUnityLoader {
36    /// Create a new serde-based Unity loader
37    pub fn new() -> Self {
38        Self
39    }
40
41    /// Load Unity YAML from a reader
42    pub fn load_from_reader<R: Read>(&self, reader: R) -> Result<Vec<UnityClass>> {
43        Ok(self.load_from_reader_detailed(reader)?.0)
44    }
45
46    pub fn load_from_reader_detailed<R: Read>(
47        &self,
48        mut reader: R,
49    ) -> Result<(Vec<UnityClass>, Vec<SerdeUnityWarning>)> {
50        // Read the entire content first
51        let mut content = String::new();
52        reader
53            .read_to_string(&mut content)
54            .map_err(|e| UnityAssetError::parse(format!("Failed to read input: {}", e)))?;
55
56        // Preprocess Unity YAML to handle Unity-specific features
57        let processed_content = self.preprocess_unity_yaml(&content)?;
58
59        // Parse YAML using serde_yaml
60        let documents: Vec<Value> = serde_yaml::Deserializer::from_str(&processed_content)
61            .map(Value::deserialize)
62            .collect::<std::result::Result<Vec<_>, _>>()
63            .map_err(|e| UnityAssetError::parse(format!("YAML parsing error: {}", e)))?;
64
65        // Convert each document to UnityClass
66        let mut unity_classes = Vec::new();
67        let mut warnings: Vec<SerdeUnityWarning> = Vec::new();
68        for (doc_index, document) in documents.iter().enumerate() {
69            match self.convert_document_to_unity_class(document, doc_index) {
70                Ok(unity_class) => unity_classes.push(unity_class),
71                Err(e) => {
72                    // Best-effort: keep parsing other documents (no stderr logging from library code).
73                    warnings.push(SerdeUnityWarning {
74                        doc_index,
75                        error: e.to_string(),
76                    });
77                }
78            }
79        }
80
81        Ok((unity_classes, warnings))
82    }
83
84    /// Load Unity YAML from a string
85    pub fn load_from_str(&self, yaml_str: &str) -> Result<Vec<UnityClass>> {
86        use std::io::Cursor;
87        let cursor = Cursor::new(yaml_str.as_bytes());
88        self.load_from_reader(cursor)
89    }
90
91    /// Load Unity YAML from an async reader
92    #[cfg(feature = "async")]
93    pub async fn load_from_async_reader<R: AsyncRead + Unpin>(
94        &self,
95        mut reader: R,
96    ) -> Result<Vec<UnityClass>> {
97        // Read the entire content first
98        let mut content = String::new();
99        reader
100            .read_to_string(&mut content)
101            .await
102            .map_err(|e| UnityAssetError::parse(format!("Failed to read input: {}", e)))?;
103
104        // Use the existing string processing logic
105        self.load_from_str(&content)
106    }
107
108    #[cfg(feature = "async")]
109    pub async fn load_from_async_reader_detailed<R: AsyncRead + Unpin>(
110        &self,
111        mut reader: R,
112    ) -> Result<(Vec<UnityClass>, Vec<SerdeUnityWarning>)> {
113        let mut content = String::new();
114        reader
115            .read_to_string(&mut content)
116            .await
117            .map_err(|e| UnityAssetError::parse(format!("Failed to read input: {}", e)))?;
118
119        self.load_from_reader_detailed(std::io::Cursor::new(content.into_bytes()))
120    }
121
122    /// Preprocess Unity YAML to handle Unity-specific features
123    fn preprocess_unity_yaml(&self, content: &str) -> Result<String> {
124        let mut processed = String::new();
125        let mut in_document = false;
126        let mut current_class_info: Option<(i32, String)> = None;
127
128        for line in content.lines() {
129            let trimmed = line.trim();
130
131            // Handle YAML directives
132            if trimmed.starts_with('%') {
133                processed.push_str(line);
134                processed.push('\n');
135                continue;
136            }
137
138            // Handle document separators
139            if trimmed.starts_with("---") {
140                in_document = true;
141
142                // Parse Unity document header: --- !u!129 &1
143                if let Some(unity_info) = self.parse_unity_document_header(trimmed) {
144                    current_class_info = Some(unity_info);
145                    // Convert to standard YAML document separator
146                    processed.push_str("---\n");
147                } else {
148                    processed.push_str(line);
149                    processed.push('\n');
150                }
151                continue;
152            }
153
154            // Handle the first line after document separator (class name)
155            if in_document
156                && !trimmed.is_empty()
157                && !trimmed.starts_with(' ')
158                && trimmed.ends_with(':')
159            {
160                if let Some((class_id, anchor)) = &current_class_info {
161                    // Add Unity metadata as special properties
162                    let class_name = trimmed.trim_end_matches(':');
163                    processed.push_str(&format!("{}:\n", class_name));
164                    processed.push_str(&format!("  __unity_class_id__: {}\n", class_id));
165                    processed.push_str(&format!("  __unity_anchor__: \"{}\"\n", anchor));
166                    current_class_info = None;
167                } else {
168                    processed.push_str(line);
169                    processed.push('\n');
170                }
171                continue;
172            }
173
174            // Regular line
175            processed.push_str(line);
176            processed.push('\n');
177        }
178
179        Ok(processed)
180    }
181
182    /// Parse Unity document header like "--- !u!129 &1"
183    fn parse_unity_document_header(&self, line: &str) -> Option<(i32, String)> {
184        let parts: Vec<&str> = line.split_whitespace().collect();
185
186        let mut class_id = 0;
187        let mut anchor = "0".to_string();
188
189        for part in parts {
190            if let Some(stripped) = part.strip_prefix("!u!") {
191                if let Ok(id) = stripped.parse::<i32>() {
192                    class_id = id;
193                }
194            } else if let Some(stripped) = part.strip_prefix('&') {
195                anchor = stripped.to_string();
196            }
197        }
198
199        if class_id > 0 {
200            Some((class_id, anchor))
201        } else {
202            None
203        }
204    }
205
206    /// Convert a YAML document to UnityClass
207    fn convert_document_to_unity_class(
208        &self,
209        document: &Value,
210        doc_index: usize,
211    ) -> Result<UnityClass> {
212        match document {
213            Value::Mapping(mapping) => {
214                // Look for Unity class structure
215                if let Some((class_key, class_value)) = mapping.iter().next() {
216                    let class_name = match class_key {
217                        Value::String(s) => s.clone(),
218                        _ => format!("Unknown_{}", doc_index),
219                    };
220
221                    // Extract Unity metadata from the class properties
222                    let (class_id, anchor, properties) =
223                        if let Value::Mapping(class_props) = class_value {
224                            let mut class_id = 0;
225                            let mut anchor = format!("doc_{}", doc_index);
226                            let mut filtered_props = IndexMap::new();
227
228                            for (key, value) in class_props {
229                                if let Value::String(key_str) = key {
230                                    match key_str.as_str() {
231                                        "__unity_class_id__" => {
232                                            if let Value::Number(n) = value
233                                                && let Some(id) = n.as_i64()
234                                            {
235                                                class_id = id as i32;
236                                            }
237                                        }
238                                        "__unity_anchor__" => {
239                                            if let Value::String(a) = value {
240                                                anchor = a.clone();
241                                            }
242                                        }
243                                        _ => {
244                                            // Regular property
245                                            let unity_value =
246                                                Self::convert_value_to_unity_value(value)?;
247                                            filtered_props.insert(key_str.clone(), unity_value);
248                                        }
249                                    }
250                                }
251                            }
252
253                            (class_id, anchor, UnityValue::Object(filtered_props))
254                        } else {
255                            let properties = Self::convert_value_to_unity_value(class_value)?;
256                            (0, format!("doc_{}", doc_index), properties)
257                        };
258
259                    // Always use the actual class name from YAML - it's more reliable than ID mapping
260                    // Unity class IDs can map to different names in different Unity versions
261                    let final_class_name = class_name;
262
263                    let mut unity_class = UnityClass::new(class_id, final_class_name, anchor);
264
265                    // Add properties
266                    if let UnityValue::Object(props) = properties {
267                        for (key, value) in props {
268                            unity_class.set(key, value);
269                        }
270                    }
271
272                    Ok(unity_class)
273                } else {
274                    // Empty mapping, create a default UnityClass
275                    Ok(UnityClass::new(
276                        0,
277                        "Unknown".to_string(),
278                        format!("doc_{}", doc_index),
279                    ))
280                }
281            }
282            _ => {
283                // Non-mapping document, treat as scalar
284                let anchor = format!("doc_{}", doc_index);
285                let mut unity_class = UnityClass::new(0, "Scalar".to_string(), anchor);
286                let value = Self::convert_value_to_unity_value(document)?;
287                unity_class.set("value".to_string(), value);
288                Ok(unity_class)
289            }
290        }
291    }
292
293    /// Convert serde_yaml Value to UnityValue
294    fn convert_value_to_unity_value(value: &Value) -> Result<UnityValue> {
295        match value {
296            Value::Null => Ok(UnityValue::Null),
297            Value::Bool(b) => Ok(UnityValue::Bool(*b)),
298            Value::Number(n) => {
299                if let Some(i) = n.as_i64() {
300                    Ok(UnityValue::Integer(i))
301                } else if let Some(f) = n.as_f64() {
302                    Ok(UnityValue::Float(f))
303                } else {
304                    Ok(UnityValue::String(n.to_string()))
305                }
306            }
307            Value::String(s) => Ok(UnityValue::String(s.clone())),
308            Value::Sequence(seq) => {
309                let mut array = Vec::new();
310                for item in seq {
311                    array.push(Self::convert_value_to_unity_value(item)?);
312                }
313                Ok(UnityValue::Array(array))
314            }
315            Value::Mapping(mapping) => {
316                let mut object = IndexMap::new();
317                for (k, v) in mapping {
318                    let key = match k {
319                        Value::String(s) => s.clone(),
320                        _ => format!("{:?}", k),
321                    };
322                    let value = Self::convert_value_to_unity_value(v)?;
323                    object.insert(key, value);
324                }
325                Ok(UnityValue::Object(object))
326            }
327            Value::Tagged(tagged) => {
328                // Handle tagged values
329                Self::convert_value_to_unity_value(&tagged.value)
330            }
331        }
332    }
333
334    /// Get Unity class name from class ID
335    /// Based on Unity's official ClassIDReference and reference libraries
336    #[allow(dead_code)]
337    fn get_class_name_from_id(&self, class_id: i32) -> String {
338        match class_id {
339            // Core runtime classes
340            0 => "Object".to_string(),
341            1 => "GameObject".to_string(),
342            2 => "Component".to_string(),
343            3 => "LevelGameManager".to_string(),
344            4 => "Transform".to_string(),
345            5 => "TimeManager".to_string(),
346            6 => "GlobalGameManager".to_string(),
347            8 => "Behaviour".to_string(),
348            9 => "GameManager".to_string(),
349            11 => "AudioManager".to_string(),
350            12 => "ParticleAnimator".to_string(),
351            13 => "InputManager".to_string(),
352            15 => "EllipsoidParticleEmitter".to_string(),
353            17 => "Pipeline".to_string(),
354            18 => "EditorExtension".to_string(),
355            19 => "Physics2DSettings".to_string(),
356            20 => "Camera".to_string(),
357            21 => "Material".to_string(),
358            23 => "MeshRenderer".to_string(),
359            25 => "Renderer".to_string(),
360            26 => "ParticleRenderer".to_string(),
361            27 => "Texture".to_string(),
362            28 => "Texture2D".to_string(),
363            29 => "OcclusionCullingSettings".to_string(),
364            30 => "GraphicsSettings".to_string(),
365            33 => "MeshFilter".to_string(),
366            41 => "OcclusionPortal".to_string(),
367            43 => "Mesh".to_string(),
368            45 => "Skybox".to_string(),
369            47 => "QualitySettings".to_string(),
370            48 => "Shader".to_string(),
371            49 => "TextAsset".to_string(),
372            50 => "Rigidbody2D".to_string(),
373            51 => "Physics2DManager".to_string(),
374            53 => "Collider2D".to_string(),
375            54 => "Rigidbody".to_string(),
376            55 => "PhysicsManager".to_string(),
377            56 => "Collider".to_string(),
378            57 => "Joint".to_string(),
379            58 => "CircleCollider2D".to_string(),
380            59 => "HingeJoint".to_string(),
381            60 => "PolygonCollider2D".to_string(),
382            61 => "BoxCollider2D".to_string(),
383            62 => "PhysicsMaterial2D".to_string(),
384            64 => "MeshCollider".to_string(),
385            65 => "BoxCollider".to_string(),
386            68 => "EdgeCollider2D".to_string(),
387            70 => "CapsuleCollider2D".to_string(),
388            72 => "ComputeShader".to_string(),
389            74 => "AnimationClip".to_string(),
390            75 => "ConstantForce".to_string(),
391            78 => "TagManager".to_string(),
392            81 => "AudioListener".to_string(),
393            82 => "AudioSource".to_string(),
394            83 => "AudioClip".to_string(),
395            84 => "RenderTexture".to_string(),
396            86 => "CustomRenderTexture".to_string(),
397            89 => "Cubemap".to_string(),
398            90 => "Avatar".to_string(),
399            91 => "AnimatorController".to_string(),
400            92 => "GUILayer".to_string(),
401            93 => "RuntimeAnimatorController".to_string(),
402            94 => "ScriptMapper".to_string(),
403            95 => "Animator".to_string(),
404            96 => "TrailRenderer".to_string(),
405            98 => "DelayedCallManager".to_string(),
406            102 => "TextMesh".to_string(),
407            104 => "RenderSettings".to_string(),
408            108 => "Light".to_string(),
409            109 => "CGProgram".to_string(),
410            110 => "BaseAnimationTrack".to_string(),
411            111 => "Animation".to_string(),
412            114 => "MonoBehaviour".to_string(),
413            115 => "MonoScript".to_string(),
414            116 => "MonoManager".to_string(),
415            117 => "Texture3D".to_string(),
416            118 => "NewAnimationTrack".to_string(),
417            119 => "Projector".to_string(),
418            120 => "LineRenderer".to_string(),
419            121 => "Flare".to_string(),
420            122 => "Halo".to_string(),
421            123 => "LensFlare".to_string(),
422            124 => "FlareLayer".to_string(),
423            125 => "HaloLayer".to_string(),
424            126 => "NavMeshAreas".to_string(),
425            127 => "HaloManager".to_string(),
426            128 => "Font".to_string(),
427            129 => "PlayerSettings".to_string(),
428            130 => "NamedObject".to_string(),
429            131 => "GUITexture".to_string(),
430            132 => "GUIText".to_string(),
431            133 => "GUIElement".to_string(),
432            134 => "PhysicMaterial".to_string(),
433            135 => "SphereCollider".to_string(),
434            136 => "CapsuleCollider".to_string(),
435            137 => "SkinnedMeshRenderer".to_string(),
436            138 => "FixedJoint".to_string(),
437            141 => "BuildSettings".to_string(),
438            142 => "AssetBundle".to_string(),
439            143 => "CharacterController".to_string(),
440            144 => "CharacterJoint".to_string(),
441            145 => "SpringJoint".to_string(),
442            146 => "WheelCollider".to_string(),
443            147 => "ResourceManager".to_string(),
444            148 => "NetworkView".to_string(),
445            149 => "NetworkManager".to_string(),
446            150 => "EllipsoidParticleEmitter".to_string(),
447            151 => "ParticleEmitter".to_string(),
448            152 => "ParticleSystem".to_string(),
449            153 => "ParticleSystemRenderer".to_string(),
450            154 => "ShaderVariantCollection".to_string(),
451            156 => "LODGroup".to_string(),
452            157 => "BlendTree".to_string(),
453            158 => "Motion".to_string(),
454            159 => "NavMeshObstacle".to_string(),
455            160 => "TerrainCollider".to_string(),
456            161 => "TerrainData".to_string(),
457            162 => "LightmapSettings".to_string(),
458            163 => "WebCamTexture".to_string(),
459            164 => "EditorSettings".to_string(),
460            165 => "InteractiveCloth".to_string(),
461            166 => "ClothRenderer".to_string(),
462            167 => "EditorUserSettings".to_string(),
463            168 => "SkinnedCloth".to_string(),
464            180 => "AudioReverbFilter".to_string(),
465            181 => "AudioHighPassFilter".to_string(),
466            182 => "AudioChorusFilter".to_string(),
467            183 => "AudioReverbZone".to_string(),
468            184 => "AudioEchoFilter".to_string(),
469            185 => "AudioLowPassFilter".to_string(),
470            186 => "AudioDistortionFilter".to_string(),
471            187 => "SparseTexture".to_string(),
472            188 => "AudioBehaviour".to_string(),
473            189 => "AudioFilter".to_string(),
474            191 => "WindZone".to_string(),
475            192 => "Cloth".to_string(),
476            193 => "SubstanceArchive".to_string(),
477            194 => "ProceduralMaterial".to_string(),
478            195 => "ProceduralTexture".to_string(),
479            196 => "Texture2DArray".to_string(),
480            197 => "CubemapArray".to_string(),
481            198 => "OffMeshLink".to_string(),
482            199 => "OcclusionArea".to_string(),
483            200 => "Tree".to_string(),
484            201 => "NavMeshAgent".to_string(),
485            202 => "NavMeshSettings".to_string(),
486            203 => "LightProbesLegacy".to_string(),
487            204 => "ParticleSystemForceField".to_string(),
488            205 => "OcclusionCullingData".to_string(),
489            206 => "NavMeshData".to_string(),
490            207 => "AudioMixer".to_string(),
491            208 => "AudioMixerController".to_string(),
492            210 => "AudioMixerGroupController".to_string(),
493            211 => "AudioMixerEffectController".to_string(),
494            212 => "AudioMixerSnapshotController".to_string(),
495            213 => "PhysicsUpdateBehaviour2D".to_string(),
496            214 => "ConstantForce2D".to_string(),
497            215 => "Effector2D".to_string(),
498            216 => "AreaEffector2D".to_string(),
499            217 => "PointEffector2D".to_string(),
500            218 => "PlatformEffector2D".to_string(),
501            219 => "SurfaceEffector2D".to_string(),
502            220 => "BuoyancyEffector2D".to_string(),
503            221 => "RelativeJoint2D".to_string(),
504            222 => "FixedJoint2D".to_string(),
505            223 => "FrictionJoint2D".to_string(),
506            224 => "TargetJoint2D".to_string(),
507            225 => "SliderJoint2D".to_string(),
508            226 => "SpringJoint2D".to_string(),
509            227 => "WheelJoint2D".to_string(),
510            228 => "ClusterInputManager".to_string(),
511            229 => "BaseVideoTexture".to_string(),
512            230 => "NavMeshObstacle".to_string(),
513            231 => "NavMeshAgent".to_string(),
514            238 => "LightProbes".to_string(),
515            240 => "LightProbeGroup".to_string(),
516            241 => "BillboardAsset".to_string(),
517            242 => "BillboardRenderer".to_string(),
518            243 => "SpeedTreeWindAsset".to_string(),
519            244 => "AnchoredJoint2D".to_string(),
520            245 => "Joint2D".to_string(),
521            246 => "SpringJoint2D".to_string(),
522            247 => "DistanceJoint2D".to_string(),
523            248 => "HingeJoint2D".to_string(),
524            249 => "SliderJoint2D".to_string(),
525            250 => "WheelJoint2D".to_string(),
526            251 => "ClusterInputManager".to_string(),
527            252 => "BaseVideoTexture".to_string(),
528            253 => "NavMeshObstacle".to_string(),
529            254 => "NavMeshAgent".to_string(),
530            258 => "OcclusionCullingData".to_string(),
531            271 => "Terrain".to_string(),
532            272 => "LightmapParameters".to_string(),
533            273 => "LightmapData".to_string(),
534            290 => "ReflectionProbe".to_string(),
535            319 => "AvatarMask".to_string(),
536            320 => "PlayableDirector".to_string(),
537            328 => "VideoPlayer".to_string(),
538            329 => "VideoClip".to_string(),
539            330 => "ParticleSystemForceField".to_string(),
540            331 => "SpriteMask".to_string(),
541            362 => "WorldAnchor".to_string(),
542            363 => "OcclusionCullingData".to_string(),
543
544            // Editor classes (1000+)
545            1001 => "PrefabInstance".to_string(),
546            1002 => "EditorExtensionImpl".to_string(),
547            1003 => "AssetImporter".to_string(),
548            1004 => "AssetDatabaseV1".to_string(),
549            1005 => "Mesh3DSImporter".to_string(),
550            1006 => "TextureImporter".to_string(),
551            1007 => "ShaderImporter".to_string(),
552            1008 => "ComputeShaderImporter".to_string(),
553            1020 => "AudioImporter".to_string(),
554            1026 => "HierarchyState".to_string(),
555            1027 => "GUIDSerializer".to_string(),
556            1028 => "AssetMetaData".to_string(),
557            1029 => "DefaultAsset".to_string(),
558            1030 => "DefaultImporter".to_string(),
559            1031 => "TextScriptImporter".to_string(),
560            1032 => "SceneAsset".to_string(),
561            1034 => "NativeFormatImporter".to_string(),
562            1035 => "MonoImporter".to_string(),
563            1040 => "AssetServerCache".to_string(),
564            1041 => "LibraryAssetImporter".to_string(),
565            1042 => "ModelImporter".to_string(),
566            1043 => "FBXImporter".to_string(),
567            1044 => "TrueTypeFontImporter".to_string(),
568            1045 => "MovieImporter".to_string(),
569            1050 => "EditorBuildSettings".to_string(),
570            1051 => "DDSImporter".to_string(),
571            1052 => "InspectorExpandedState".to_string(),
572            1053 => "AnnotationManager".to_string(),
573            1054 => "PluginImporter".to_string(),
574            1055 => "EditorUserBuildSettings".to_string(),
575            1056 => "PVRImporter".to_string(),
576            1057 => "ASTCImporter".to_string(),
577            1058 => "KTXImporter".to_string(),
578            1101 => "AnimatorStateTransition".to_string(),
579            1102 => "AnimatorState".to_string(),
580            1107 => "HumanTemplate".to_string(),
581            1108 => "AnimatorStateMachine".to_string(),
582            1109 => "PreviewAssetType".to_string(),
583            1110 => "AnimatorTransition".to_string(),
584            1111 => "SpeedTreeImporter".to_string(),
585            1112 => "AnimatorTransitionBase".to_string(),
586            1113 => "SubstanceImporter".to_string(),
587            1114 => "LightmapParameters".to_string(),
588            1115 => "LightmapSnapshot".to_string(),
589            1120 => "SketchUpImporter".to_string(),
590            1124 => "BuildReport".to_string(),
591            1125 => "PackedAssets".to_string(),
592            1126 => "VideoClipImporter".to_string(),
593
594            // Special large IDs
595            19719996 => "TilemapCollider2D".to_string(),
596            41386430 => "AssetImporterLog".to_string(),
597            73398921 => "VFXRenderer".to_string(),
598            76251197 => "SerializableManagedRefTestClass".to_string(),
599            156049354 => "Grid".to_string(),
600            156483287 => "ScenesUsingAssets".to_string(),
601            171741748 => "ArticulationBody".to_string(),
602            181963792 => "Preset".to_string(),
603            277625683 => "EmptyObject".to_string(),
604            285090594 => "IConstraint".to_string(),
605            687078895 => "SpriteAtlas".to_string(),
606
607            // Unknown class ID
608            _ => format!("UnityClass_{}", class_id),
609        }
610    }
611}
612
613impl Default for SerdeUnityLoader {
614    fn default() -> Self {
615        Self::new()
616    }
617}
618
619#[cfg(test)]
620mod tests {
621    use super::*;
622
623    #[test]
624    fn test_serde_loader_creation() {
625        let _loader = SerdeUnityLoader::new();
626        // Just test creation
627    }
628
629    #[test]
630    fn test_load_simple_yaml() {
631        let loader = SerdeUnityLoader::new();
632        let yaml = r#"
633test_key: test_value
634number: 42
635boolean: true
636"#;
637
638        let result = loader.load_from_str(yaml);
639        assert!(result.is_ok());
640
641        let classes = result.unwrap();
642        assert!(!classes.is_empty());
643    }
644
645    #[test]
646    fn test_load_unity_gameobject() {
647        let loader = SerdeUnityLoader::new();
648        let yaml = r#"
649GameObject:
650  m_Name: Player
651  m_IsActive: 1
652"#;
653
654        let result = loader.load_from_str(yaml);
655        assert!(result.is_ok());
656
657        let classes = result.unwrap();
658        assert_eq!(classes.len(), 1);
659
660        let class = &classes[0];
661        if let Some(UnityValue::String(name)) = class.get("m_Name") {
662            assert_eq!(name, "Player");
663        }
664    }
665}