1use 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
32pub struct SerdeUnityLoader;
34
35impl SerdeUnityLoader {
36 pub fn new() -> Self {
38 Self
39 }
40
41 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 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 let processed_content = self.preprocess_unity_yaml(&content)?;
58
59 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 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 warnings.push(SerdeUnityWarning {
74 doc_index,
75 error: e.to_string(),
76 });
77 }
78 }
79 }
80
81 Ok((unity_classes, warnings))
82 }
83
84 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 #[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 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 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 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 if trimmed.starts_with('%') {
133 processed.push_str(line);
134 processed.push('\n');
135 continue;
136 }
137
138 if trimmed.starts_with("---") {
140 in_document = true;
141
142 if let Some(unity_info) = self.parse_unity_document_header(trimmed) {
144 current_class_info = Some(unity_info);
145 processed.push_str("---\n");
147 } else {
148 processed.push_str(line);
149 processed.push('\n');
150 }
151 continue;
152 }
153
154 if in_document
156 && !trimmed.is_empty()
157 && !trimmed.starts_with(' ')
158 && trimmed.ends_with(':')
159 {
160 if let Some((class_id, anchor)) = ¤t_class_info {
161 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 processed.push_str(line);
176 processed.push('\n');
177 }
178
179 Ok(processed)
180 }
181
182 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 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 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 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 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 let final_class_name = class_name;
262
263 let mut unity_class = UnityClass::new(class_id, final_class_name, anchor);
264
265 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 Ok(UnityClass::new(
276 0,
277 "Unknown".to_string(),
278 format!("doc_{}", doc_index),
279 ))
280 }
281 }
282 _ => {
283 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 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 Self::convert_value_to_unity_value(&tagged.value)
330 }
331 }
332 }
333
334 #[allow(dead_code)]
337 fn get_class_name_from_id(&self, class_id: i32) -> String {
338 match class_id {
339 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 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 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 _ => 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 }
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}