1use super::types::*;
7use crate::asset::SerializedFile;
8use crate::error::Result;
9use crate::reader::BinaryReader;
10use crate::typetree::{TypeTree, TypeTreeSerializer};
11use std::collections::{HashMap, HashSet};
12use unity_asset_core::UnityValue;
13
14pub struct DependencyAnalyzer {
19 dependency_cache: HashMap<i64, Vec<i64>>,
21 pptr_dependency_cache: HashMap<((usize, usize, usize), i64), ExtractedDependencies>,
23 reverse_dependency_cache: HashMap<i64, Vec<i64>>,
25}
26
27#[derive(Debug, Clone, Default)]
28struct ExtractedDependencies {
29 internal: Vec<i64>,
30 external: Vec<(i32, i64)>,
31}
32
33type CachedPptrDependencies = (Vec<i64>, Vec<(i32, i64)>);
34
35impl DependencyAnalyzer {
36 pub fn new() -> Self {
38 Self {
39 dependency_cache: HashMap::new(),
40 pptr_dependency_cache: HashMap::new(),
41 reverse_dependency_cache: HashMap::new(),
42 }
43 }
44
45 pub fn analyze_dependencies(
50 &mut self,
51 objects: &[&crate::asset::ObjectInfo],
52 ) -> Result<DependencyInfo> {
53 let mut internal_refs = Vec::new();
54 let mut all_nodes = HashSet::new();
55 let mut edges = Vec::new();
56
57 for obj in objects {
59 all_nodes.insert(obj.path_id);
60 }
61
62 let _ = &mut internal_refs;
65 let _ = &mut edges;
66
67 let external_refs = Vec::new();
68
69 let nodes: Vec<i64> = all_nodes.into_iter().collect();
71 let root_objects = self.find_root_objects(&nodes, &edges);
72 let leaf_objects = self.find_leaf_objects(&nodes, &edges);
73
74 let dependency_graph = DependencyGraph {
75 nodes,
76 edges,
77 root_objects,
78 leaf_objects,
79 };
80
81 let circular_deps = self.detect_circular_dependencies(&dependency_graph)?;
83
84 Ok(DependencyInfo {
85 external_references: external_refs,
86 internal_references: internal_refs,
87 dependency_graph,
88 circular_dependencies: circular_deps,
89 })
90 }
91
92 pub fn analyze_dependencies_in_asset(
97 &mut self,
98 asset: &SerializedFile,
99 objects: &[&crate::asset::ObjectInfo],
100 ) -> Result<DependencyInfo> {
101 let mut external_ref_map: HashMap<(i32, i64), Vec<i64>> = HashMap::new();
102 let mut internal_refs = Vec::new();
103 let mut all_nodes = HashSet::new();
104 let mut edges = Vec::new();
105
106 for obj in objects {
107 all_nodes.insert(obj.path_id);
108 }
109
110 for obj in objects {
111 let deps = self.extract_object_dependencies_in_asset(asset, obj)?;
112
113 for dep_id in deps.internal {
114 if all_nodes.contains(&dep_id) {
115 internal_refs.push(InternalReference {
116 from_object: obj.path_id,
117 to_object: dep_id,
118 reference_type: "Direct".to_string(),
119 });
120 edges.push((obj.path_id, dep_id));
121 } else {
122 external_ref_map
123 .entry((0, dep_id))
124 .or_default()
125 .push(obj.path_id);
126 }
127 }
128
129 for (file_id, path_id) in deps.external {
130 external_ref_map
131 .entry((file_id, path_id))
132 .or_default()
133 .push(obj.path_id);
134 }
135 }
136
137 let external_refs = external_ref_map
138 .into_iter()
139 .map(|((file_id, path_id), mut referenced_by)| {
140 referenced_by.sort_unstable();
141 referenced_by.dedup();
142 let (file_path, guid) = resolve_external_file(asset, file_id);
143 ExternalReference {
144 file_id,
145 path_id,
146 referenced_by,
147 file_path,
148 guid,
149 }
150 })
151 .collect();
152
153 let nodes: Vec<i64> = all_nodes.into_iter().collect();
154 let root_objects = self.find_root_objects(&nodes, &edges);
155 let leaf_objects = self.find_leaf_objects(&nodes, &edges);
156
157 let dependency_graph = DependencyGraph {
158 nodes,
159 edges,
160 root_objects,
161 leaf_objects,
162 };
163
164 let circular_deps = self.detect_circular_dependencies(&dependency_graph)?;
165
166 Ok(DependencyInfo {
167 external_references: external_refs,
168 internal_references: internal_refs,
169 dependency_graph,
170 circular_dependencies: circular_deps,
171 })
172 }
173
174 fn extract_object_dependencies_in_asset(
176 &mut self,
177 asset: &SerializedFile,
178 obj: &crate::asset::ObjectInfo,
179 ) -> Result<ExtractedDependencies> {
180 let file_key = asset.data_identity_key();
181
182 if let Some(cached) = self.pptr_dependency_cache.get(&(file_key, obj.path_id)) {
183 return Ok(cached.clone());
184 }
185
186 let mut deps = ExtractedDependencies::default();
187
188 if asset.enable_type_tree
189 && let Some(tree) = type_tree_for_object(asset, obj)
190 && !tree.is_empty()
191 {
192 if let Ok(scanned) = scan_object_pptrs_with_typetree(asset, obj, tree) {
196 deps = scanned;
197 } else if let Ok(values) = parse_object_with_typetree(asset, obj, tree) {
198 scan_pptr_in_value(&UnityValue::Object(values), &mut deps);
200 }
201 }
202
203 deps.internal.sort_unstable();
204 deps.internal.dedup();
205 deps.external.sort_unstable();
206 deps.external.dedup();
207
208 self.pptr_dependency_cache
209 .insert((file_key, obj.path_id), deps.clone());
210
211 Ok(deps)
212 }
213
214 fn find_root_objects(&self, nodes: &[i64], edges: &[(i64, i64)]) -> Vec<i64> {
216 let mut has_incoming: HashSet<i64> = HashSet::new();
217
218 for (_, to) in edges {
219 has_incoming.insert(*to);
220 }
221
222 nodes
223 .iter()
224 .filter(|node| !has_incoming.contains(node))
225 .copied()
226 .collect()
227 }
228
229 fn find_leaf_objects(&self, nodes: &[i64], edges: &[(i64, i64)]) -> Vec<i64> {
231 let mut has_outgoing: HashSet<i64> = HashSet::new();
232
233 for (from, _) in edges {
234 has_outgoing.insert(*from);
235 }
236
237 nodes
238 .iter()
239 .filter(|node| !has_outgoing.contains(node))
240 .copied()
241 .collect()
242 }
243
244 fn detect_circular_dependencies(&self, graph: &DependencyGraph) -> Result<Vec<Vec<i64>>> {
246 let mut visited = HashSet::new();
247 let mut rec_stack = HashSet::new();
248 let mut cycles = Vec::new();
249
250 let mut adj_list: HashMap<i64, Vec<i64>> = HashMap::new();
252 for node in &graph.nodes {
253 adj_list.insert(*node, Vec::new());
254 }
255 for (from, to) in &graph.edges {
256 adj_list.get_mut(from).unwrap().push(*to);
257 }
258
259 for &node in &graph.nodes {
261 if !visited.contains(&node) {
262 let mut path = Vec::new();
263 Self::dfs_detect_cycle(
264 node,
265 &adj_list,
266 &mut visited,
267 &mut rec_stack,
268 &mut path,
269 &mut cycles,
270 );
271 }
272 }
273
274 Ok(cycles)
275 }
276
277 fn dfs_detect_cycle(
279 node: i64,
280 adj_list: &HashMap<i64, Vec<i64>>,
281 visited: &mut HashSet<i64>,
282 rec_stack: &mut HashSet<i64>,
283 path: &mut Vec<i64>,
284 cycles: &mut Vec<Vec<i64>>,
285 ) {
286 visited.insert(node);
287 rec_stack.insert(node);
288 path.push(node);
289
290 if let Some(neighbors) = adj_list.get(&node) {
291 for &neighbor in neighbors {
292 if !visited.contains(&neighbor) {
293 Self::dfs_detect_cycle(neighbor, adj_list, visited, rec_stack, path, cycles);
294 } else if rec_stack.contains(&neighbor) {
295 if let Some(cycle_start) = path.iter().position(|&x| x == neighbor) {
297 let cycle = path[cycle_start..].to_vec();
298 cycles.push(cycle);
299 }
300 }
301 }
302 }
303
304 path.pop();
305 rec_stack.remove(&node);
306 }
307
308 pub fn clear_cache(&mut self) {
310 self.dependency_cache.clear();
311 self.pptr_dependency_cache.clear();
312 self.reverse_dependency_cache.clear();
313 }
314
315 pub fn get_cached_dependencies(&self, object_id: i64) -> Option<&Vec<i64>> {
317 self.dependency_cache.get(&object_id)
318 }
319
320 pub fn get_cached_dependencies_in_asset(
322 &self,
323 asset: &SerializedFile,
324 object_id: i64,
325 ) -> Option<CachedPptrDependencies> {
326 let file_key = asset.data_identity_key();
327 self.pptr_dependency_cache
328 .get(&(file_key, object_id))
329 .map(|deps| (deps.internal.clone(), deps.external.clone()))
330 }
331}
332
333impl Default for DependencyAnalyzer {
334 fn default() -> Self {
335 Self::new()
336 }
337}
338
339fn type_tree_for_object<'a>(
340 asset: &'a SerializedFile,
341 info: &crate::asset::ObjectInfo,
342) -> Option<&'a TypeTree> {
343 if info.type_index >= 0 {
344 return asset
345 .types
346 .get(info.type_index as usize)
347 .map(|t| &t.type_tree);
348 }
349
350 asset
351 .types
352 .iter()
353 .find(|t| t.class_id == info.type_id)
354 .map(|t| &t.type_tree)
355}
356
357fn parse_object_with_typetree(
358 asset: &SerializedFile,
359 info: &crate::asset::ObjectInfo,
360 tree: &TypeTree,
361) -> Result<indexmap::IndexMap<String, UnityValue>> {
362 let bytes = asset.object_bytes(info)?;
363 let mut reader = BinaryReader::new(bytes, asset.header.byte_order());
364 let serializer = TypeTreeSerializer::new(tree);
365 if asset.ref_types.is_empty() {
366 serializer.parse_object(&mut reader)
367 } else {
368 serializer.parse_object_with_ref_types(&mut reader, &asset.ref_types)
369 }
370}
371
372fn scan_object_pptrs_with_typetree(
373 asset: &SerializedFile,
374 info: &crate::asset::ObjectInfo,
375 tree: &TypeTree,
376) -> Result<ExtractedDependencies> {
377 let bytes = asset.object_bytes(info)?;
378 let mut reader = BinaryReader::new(bytes, asset.header.byte_order());
379 let serializer = TypeTreeSerializer::new(tree);
380 let scan = if asset.ref_types.is_empty() {
381 serializer.scan_pptrs(&mut reader)?
382 } else {
383 serializer.scan_pptrs_with_ref_types(&mut reader, Some(&asset.ref_types))?
384 };
385
386 Ok(ExtractedDependencies {
387 internal: scan.internal,
388 external: scan.external,
389 })
390}
391
392fn resolve_external_file(
393 asset: &SerializedFile,
394 file_id: i32,
395) -> (Option<String>, Option<[u8; 16]>) {
396 if file_id <= 0 {
397 return (None, None);
398 }
399
400 let idx = (file_id - 1) as usize;
401 let Some(ext) = asset.externals.get(idx) else {
402 return (None, None);
403 };
404
405 let file_path = if ext.path.is_empty() {
406 None
407 } else {
408 Some(ext.path.clone())
409 };
410 let guid = Some(ext.guid);
411 (file_path, guid)
412}
413
414fn scan_pptr_in_value(value: &UnityValue, deps: &mut ExtractedDependencies) {
415 match value {
416 UnityValue::Array(items) => {
417 for item in items {
418 scan_pptr_in_value(item, deps);
419 }
420 }
421 UnityValue::Object(obj) => {
422 if let Some((file_id, path_id)) = try_read_pptr(obj)
423 && path_id != 0
424 {
425 if file_id == 0 {
426 deps.internal.push(path_id);
427 } else {
428 deps.external.push((file_id, path_id));
429 }
430 }
431
432 for (_, v) in obj.iter() {
433 scan_pptr_in_value(v, deps);
434 }
435 }
436 _ => {}
437 }
438}
439
440fn try_read_pptr(map: &indexmap::IndexMap<String, UnityValue>) -> Option<(i32, i64)> {
441 let file_id = get_i32_ci(map, &["fileID", "m_FileID"])?;
442 let path_id = get_i64_ci(map, &["pathID", "m_PathID"])?;
443 Some((file_id, path_id))
444}
445
446fn extract_gameobject_components(props: &indexmap::IndexMap<String, UnityValue>) -> Vec<i64> {
447 let Some(UnityValue::Array(items)) = props.get("m_Component") else {
448 return Vec::new();
449 };
450
451 let mut out = Vec::new();
452 for item in items {
453 let UnityValue::Object(obj) = item else {
454 continue;
455 };
456
457 if let Some(UnityValue::Object(component_obj)) = obj.get("component") {
459 if let Some((file_id, path_id)) = try_read_pptr(component_obj)
460 && file_id == 0
461 && path_id != 0
462 {
463 out.push(path_id);
464 }
465 continue;
466 }
467
468 if let Some((file_id, path_id)) = try_read_pptr(obj)
470 && file_id == 0
471 && path_id != 0
472 {
473 out.push(path_id);
474 }
475 }
476 out
477}
478
479fn extract_transform_gameobject(props: &indexmap::IndexMap<String, UnityValue>) -> Option<i64> {
480 let value = props.get("m_GameObject")?;
481 extract_internal_path_id(value)
482}
483
484fn extract_transform_parent(props: &indexmap::IndexMap<String, UnityValue>) -> Option<i64> {
485 let value = props.get("m_Father")?;
486 extract_internal_path_id(value)
487}
488
489fn extract_transform_children(props: &indexmap::IndexMap<String, UnityValue>) -> Vec<i64> {
490 let Some(UnityValue::Array(items)) = props.get("m_Children") else {
491 return Vec::new();
492 };
493
494 let mut out = Vec::new();
495 for item in items {
496 if let Some(path_id) = extract_internal_path_id(item)
497 && path_id != 0
498 {
499 out.push(path_id);
500 }
501 }
502 out
503}
504
505fn extract_internal_path_id(value: &UnityValue) -> Option<i64> {
506 match value {
507 UnityValue::Object(obj) => {
508 let (file_id, path_id) = try_read_pptr(obj)?;
509 if file_id == 0 { Some(path_id) } else { None }
510 }
511 _ => None,
512 }
513}
514
515fn get_i32_ci(map: &indexmap::IndexMap<String, UnityValue>, keys: &[&str]) -> Option<i32> {
516 for key in keys {
517 for (k, v) in map.iter() {
518 if k.eq_ignore_ascii_case(key) {
519 return match v {
520 UnityValue::Integer(i) => Some(*i as i32),
521 UnityValue::Float(f) => Some(*f as i32),
522 _ => None,
523 };
524 }
525 }
526 }
527 None
528}
529
530fn get_i64_ci(map: &indexmap::IndexMap<String, UnityValue>, keys: &[&str]) -> Option<i64> {
531 for key in keys {
532 for (k, v) in map.iter() {
533 if k.eq_ignore_ascii_case(key) {
534 return match v {
535 UnityValue::Integer(i) => Some(*i),
536 UnityValue::Float(f) => Some(*f as i64),
537 _ => None,
538 };
539 }
540 }
541 }
542 None
543}
544
545pub struct RelationshipAnalyzer {
550 hierarchy_cache: HashMap<i64, GameObjectHierarchy>,
552}
553
554impl RelationshipAnalyzer {
555 pub fn new() -> Self {
557 Self {
558 hierarchy_cache: HashMap::new(),
559 }
560 }
561
562 pub fn analyze_relationships(
564 &mut self,
565 objects: &[&crate::asset::ObjectInfo],
566 ) -> Result<AssetRelationships> {
567 let mut gameobject_hierarchy = Vec::new();
568 let mut component_relationships = Vec::new();
569 let mut asset_references = Vec::new();
570
571 let mut gameobjects = Vec::new();
573 let mut transforms = Vec::new();
574 let mut components = Vec::new();
575 let mut assets = Vec::new();
576
577 for obj in objects {
578 match obj.type_id {
579 class_ids::GAME_OBJECT => gameobjects.push(obj),
580 class_ids::TRANSFORM => transforms.push(obj),
581 class_ids::COMPONENT | class_ids::BEHAVIOUR | class_ids::MONO_BEHAVIOUR => {
582 components.push(obj)
583 }
584 _ => assets.push(obj),
585 }
586 }
587
588 for go in gameobjects {
590 let hierarchy = GameObjectHierarchy {
591 gameobject_id: go.path_id,
592 name: format!("GameObject_{}", go.path_id),
593 parent_id: None,
594 children_ids: Vec::new(),
595 transform_id: 0,
596 components: Vec::new(),
597 depth: 0,
598 };
599 gameobject_hierarchy.push(hierarchy);
600 }
601
602 for comp in components {
604 if let Ok(relationship) = self.analyze_component_relationship(comp) {
605 component_relationships.push(relationship);
606 }
607 }
608
609 for asset in assets {
611 if let Ok(reference) = self.analyze_asset_reference(asset) {
612 asset_references.push(reference);
613 }
614 }
615
616 Ok(AssetRelationships {
617 gameobject_hierarchy,
618 component_relationships,
619 asset_references,
620 })
621 }
622
623 pub fn analyze_relationships_in_asset(
629 &mut self,
630 asset: &SerializedFile,
631 objects: &[&crate::asset::ObjectInfo],
632 ) -> Result<AssetRelationships> {
633 if !asset.enable_type_tree {
634 return self.analyze_relationships(objects);
635 }
636
637 let mut by_path_id: HashMap<i64, &crate::asset::ObjectInfo> = HashMap::new();
638 for obj in objects {
639 by_path_id.insert(obj.path_id, *obj);
640 }
641
642 let mut gameobject_props: HashMap<i64, indexmap::IndexMap<String, UnityValue>> =
643 HashMap::new();
644 let mut transform_props: HashMap<i64, indexmap::IndexMap<String, UnityValue>> =
645 HashMap::new();
646
647 for obj in objects {
648 match obj.type_id {
649 class_ids::GAME_OBJECT => {
650 if let Some(tree) = type_tree_for_object(asset, obj)
651 && !tree.is_empty()
652 && let Ok(values) = parse_object_with_typetree(asset, obj, tree)
653 {
654 gameobject_props.insert(obj.path_id, values);
655 }
656 }
657 class_ids::TRANSFORM => {
658 if let Some(tree) = type_tree_for_object(asset, obj)
659 && !tree.is_empty()
660 && let Ok(values) = parse_object_with_typetree(asset, obj, tree)
661 {
662 transform_props.insert(obj.path_id, values);
663 }
664 }
665 _ => {}
666 }
667 }
668
669 let mut go_name: HashMap<i64, String> = HashMap::new();
671 let mut go_components: HashMap<i64, Vec<i64>> = HashMap::new();
672 let mut go_transform: HashMap<i64, i64> = HashMap::new();
673
674 for (go_id, props) in &gameobject_props {
675 let name = props
676 .get("m_Name")
677 .and_then(|v| match v {
678 UnityValue::String(s) => Some(s.clone()),
679 _ => None,
680 })
681 .unwrap_or_else(|| format!("GameObject_{}", go_id));
682 go_name.insert(*go_id, name);
683
684 let components = extract_gameobject_components(props);
685 if !components.is_empty() {
686 go_components.insert(*go_id, components.clone());
687
688 for component_id in components {
690 if let Some(info) = by_path_id.get(&component_id)
691 && info.type_id == class_ids::TRANSFORM
692 {
693 go_transform.insert(*go_id, component_id);
694 break;
695 }
696 }
697 } else {
698 go_components.insert(*go_id, Vec::new());
699 }
700 }
701
702 let mut transform_to_go: HashMap<i64, i64> = HashMap::new();
704 let mut transform_parent: HashMap<i64, i64> = HashMap::new();
705 let mut transform_children: HashMap<i64, Vec<i64>> = HashMap::new();
706
707 for (tr_id, props) in &transform_props {
708 if let Some(go_id) = extract_transform_gameobject(props) {
709 transform_to_go.insert(*tr_id, go_id);
710 go_transform.entry(go_id).or_insert(*tr_id);
711 }
712
713 if let Some(parent_id) = extract_transform_parent(props) {
714 transform_parent.insert(*tr_id, parent_id);
715 }
716
717 let children = extract_transform_children(props);
718 if !children.is_empty() {
719 transform_children.insert(*tr_id, children);
720 }
721 }
722
723 let mut hierarchies: HashMap<i64, GameObjectHierarchy> = HashMap::new();
725 for go_id in gameobject_props.keys() {
726 let transform_id = go_transform.get(go_id).copied().unwrap_or(0);
727 let parent_id = if transform_id != 0 {
728 transform_parent
729 .get(&transform_id)
730 .and_then(|pid| transform_to_go.get(pid))
731 .copied()
732 } else {
733 None
734 };
735
736 let mut children_ids = Vec::new();
737 if transform_id != 0
738 && let Some(children) = transform_children.get(&transform_id)
739 {
740 for child_tr in children {
741 if let Some(child_go) = transform_to_go.get(child_tr) {
742 children_ids.push(*child_go);
743 }
744 }
745 }
746 children_ids.sort_unstable();
747 children_ids.dedup();
748
749 let mut comps = go_components.get(go_id).cloned().unwrap_or_default();
750 comps.sort_unstable();
751 comps.dedup();
752
753 hierarchies.insert(
754 *go_id,
755 GameObjectHierarchy {
756 gameobject_id: *go_id,
757 name: go_name
758 .get(go_id)
759 .cloned()
760 .unwrap_or_else(|| format!("GameObject_{}", go_id)),
761 parent_id,
762 children_ids,
763 transform_id,
764 components: comps,
765 depth: 0,
766 },
767 );
768 }
769
770 let mut roots: Vec<i64> = Vec::new();
772 for (id, h) in &hierarchies {
773 match h.parent_id {
774 None => roots.push(*id),
775 Some(pid) if !hierarchies.contains_key(&pid) => roots.push(*id),
776 _ => {}
777 }
778 }
779 roots.sort_unstable();
780 roots.dedup();
781
782 let mut queue: std::collections::VecDeque<(i64, u32)> = std::collections::VecDeque::new();
783 for r in roots {
784 queue.push_back((r, 0));
785 }
786 let mut visited: HashSet<i64> = HashSet::new();
787 while let Some((node, depth)) = queue.pop_front() {
788 if !visited.insert(node) {
789 continue;
790 }
791 if let Some(entry) = hierarchies.get_mut(&node) {
792 entry.depth = depth;
793 for child in entry.children_ids.clone() {
794 queue.push_back((child, depth.saturating_add(1)));
795 }
796 }
797 }
798
799 let mut component_relationships = Vec::new();
801 for (go_id, comp_ids) in &go_components {
802 for comp_id in comp_ids {
803 let component_type = by_path_id
804 .get(comp_id)
805 .map(|info| self.get_component_type_name(info.type_id))
806 .unwrap_or_else(|| format!("Component_{}", comp_id));
807
808 component_relationships.push(ComponentRelationship {
809 component_id: *comp_id,
810 component_type,
811 gameobject_id: *go_id,
812 dependencies: Vec::new(),
813 external_dependencies: Vec::new(),
814 });
815 }
816 }
817
818 Ok(AssetRelationships {
820 gameobject_hierarchy: hierarchies.into_values().collect(),
821 component_relationships,
822 asset_references: Vec::new(),
823 })
824 }
825
826 #[allow(dead_code)]
828 fn analyze_gameobject_hierarchy(
829 &mut self,
830 gameobject: &crate::asset::ObjectInfo,
831 _transforms: &[&crate::asset::ObjectInfo],
832 ) -> Result<GameObjectHierarchy> {
833 Ok(GameObjectHierarchy {
837 gameobject_id: gameobject.path_id,
838 name: format!("GameObject_{}", gameobject.path_id),
839 parent_id: None,
840 children_ids: Vec::new(),
841 transform_id: 0, components: Vec::new(),
843 depth: 0,
844 })
845 }
846
847 fn analyze_component_relationship(
849 &self,
850 component: &crate::asset::ObjectInfo,
851 ) -> Result<ComponentRelationship> {
852 Ok(ComponentRelationship {
855 component_id: component.path_id,
856 component_type: self.get_component_type_name(component.type_id),
857 gameobject_id: 0, dependencies: Vec::new(),
859 external_dependencies: Vec::new(),
860 })
861 }
862
863 fn analyze_asset_reference(&self, asset: &crate::asset::ObjectInfo) -> Result<AssetReference> {
865 Ok(AssetReference {
868 asset_id: asset.path_id,
869 asset_type: self.get_asset_type_name(asset.type_id),
870 referenced_by: Vec::new(),
871 file_path: None,
872 })
873 }
874
875 fn get_component_type_name(&self, type_id: i32) -> String {
877 match type_id {
878 class_ids::TRANSFORM => "Transform".to_string(),
879 class_ids::MONO_BEHAVIOUR => "MonoBehaviour".to_string(),
880 _ => format!("Component_{}", type_id),
881 }
882 }
883
884 fn get_asset_type_name(&self, type_id: i32) -> String {
886 match type_id {
887 class_ids::TEXTURE_2D => "Texture2D".to_string(),
888 class_ids::MESH => "Mesh".to_string(),
889 class_ids::MATERIAL => "Material".to_string(),
890 class_ids::AUDIO_CLIP => "AudioClip".to_string(),
891 class_ids::SPRITE => "Sprite".to_string(),
892 _ => format!("Asset_{}", type_id),
893 }
894 }
895
896 pub fn clear_cache(&mut self) {
898 self.hierarchy_cache.clear();
899 }
900}
901
902impl Default for RelationshipAnalyzer {
903 fn default() -> Self {
904 Self::new()
905 }
906}
907
908#[cfg(test)]
909mod tests {
910 use super::*;
911 use indexmap::IndexMap;
912
913 #[test]
914 fn test_dependency_analyzer_creation() {
915 let analyzer = DependencyAnalyzer::new();
916 assert!(analyzer.dependency_cache.is_empty());
917 }
918
919 #[test]
920 fn test_relationship_analyzer_creation() {
921 let analyzer = RelationshipAnalyzer::new();
922 assert!(analyzer.hierarchy_cache.is_empty());
923 }
924
925 #[test]
926 fn test_root_leaf_detection() {
927 let analyzer = DependencyAnalyzer::new();
928 let nodes = vec![1, 2, 3, 4];
929 let edges = vec![(1, 2), (2, 3), (4, 3)];
930
931 let roots = analyzer.find_root_objects(&nodes, &edges);
932 let leaves = analyzer.find_leaf_objects(&nodes, &edges);
933
934 assert!(roots.contains(&1));
935 assert!(roots.contains(&4));
936 assert!(leaves.contains(&3));
937 }
938
939 #[test]
940 fn test_scan_pptr_variants() {
941 let mut deps = ExtractedDependencies::default();
942
943 let mut pptr_internal = IndexMap::new();
945 pptr_internal.insert("fileID".to_string(), UnityValue::Integer(0));
946 pptr_internal.insert("pathID".to_string(), UnityValue::Integer(123));
947
948 let mut pptr_external = IndexMap::new();
950 pptr_external.insert("m_FileID".to_string(), UnityValue::Integer(2));
951 pptr_external.insert("m_PathID".to_string(), UnityValue::Integer(999));
952
953 let mut root = IndexMap::new();
954 root.insert("a".to_string(), UnityValue::Object(pptr_internal));
955 root.insert(
956 "b".to_string(),
957 UnityValue::Array(vec![UnityValue::Object(pptr_external)]),
958 );
959
960 scan_pptr_in_value(&UnityValue::Object(root), &mut deps);
961 deps.internal.sort_unstable();
962 deps.internal.dedup();
963 deps.external.sort_unstable();
964 deps.external.dedup();
965
966 assert_eq!(deps.internal, vec![123]);
967 assert_eq!(deps.external, vec![(2, 999)]);
968 }
969
970 #[test]
971 fn test_extract_gameobject_components_and_transform_links() {
972 let mut pptr1 = IndexMap::new();
974 pptr1.insert("fileID".to_string(), UnityValue::Integer(0));
975 pptr1.insert("pathID".to_string(), UnityValue::Integer(10));
976 let mut item1 = IndexMap::new();
977 item1.insert("component".to_string(), UnityValue::Object(pptr1));
978
979 let mut pptr2 = IndexMap::new();
980 pptr2.insert("fileID".to_string(), UnityValue::Integer(0));
981 pptr2.insert("pathID".to_string(), UnityValue::Integer(11));
982 let mut item2 = IndexMap::new();
983 item2.insert("component".to_string(), UnityValue::Object(pptr2));
984
985 let mut go_props = IndexMap::new();
986 go_props.insert(
987 "m_Component".to_string(),
988 UnityValue::Array(vec![UnityValue::Object(item1), UnityValue::Object(item2)]),
989 );
990 let comps = extract_gameobject_components(&go_props);
991 assert_eq!(comps, vec![10, 11]);
992
993 let mut go_pptr = IndexMap::new();
995 go_pptr.insert("fileID".to_string(), UnityValue::Integer(0));
996 go_pptr.insert("pathID".to_string(), UnityValue::Integer(100));
997
998 let mut parent_pptr = IndexMap::new();
999 parent_pptr.insert("fileID".to_string(), UnityValue::Integer(0));
1000 parent_pptr.insert("pathID".to_string(), UnityValue::Integer(200));
1001
1002 let mut child_pptr = IndexMap::new();
1003 child_pptr.insert("fileID".to_string(), UnityValue::Integer(0));
1004 child_pptr.insert("pathID".to_string(), UnityValue::Integer(300));
1005
1006 let mut tr_props = IndexMap::new();
1007 tr_props.insert("m_GameObject".to_string(), UnityValue::Object(go_pptr));
1008 tr_props.insert("m_Father".to_string(), UnityValue::Object(parent_pptr));
1009 tr_props.insert(
1010 "m_Children".to_string(),
1011 UnityValue::Array(vec![UnityValue::Object(child_pptr)]),
1012 );
1013
1014 assert_eq!(extract_transform_gameobject(&tr_props), Some(100));
1015 assert_eq!(extract_transform_parent(&tr_props), Some(200));
1016 assert_eq!(extract_transform_children(&tr_props), vec![300]);
1017 }
1018}