unity_asset_binary/metadata/
analyzer.rs

1//! Dependency and relationship analysis
2//!
3//! This module provides advanced analysis capabilities for Unity assets,
4//! including dependency tracking and relationship mapping.
5
6use 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
14/// Dependency analyzer for Unity assets
15///
16/// This struct provides methods for analyzing dependencies and relationships
17/// between Unity objects within and across assets.
18pub struct DependencyAnalyzer {
19    /// Cache for analyzed dependencies
20    dependency_cache: HashMap<i64, Vec<i64>>,
21    /// Cache for analyzed dependencies (TypeTree + PPtr scan), keyed by (asset identity, path_id)
22    pptr_dependency_cache: HashMap<((usize, usize, usize), i64), ExtractedDependencies>,
23    /// Cache for reverse dependencies
24    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    /// Create a new dependency analyzer
37    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    /// Analyze dependencies for a set of objects
46    ///
47    /// Note: this legacy API is a placeholder and returns no dependencies.
48    /// Use `analyze_dependencies_in_asset` for real TypeTree-based scanning.
49    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        // First pass: collect all object IDs
58        for obj in objects {
59            all_nodes.insert(obj.path_id);
60        }
61
62        // Placeholder implementation (kept for backward compatibility).
63        // Previously this always returned empty dependencies.
64        let _ = &mut internal_refs;
65        let _ = &mut edges;
66
67        let external_refs = Vec::new();
68
69        // Build dependency graph
70        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        // Detect circular dependencies
82        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    /// Analyze dependencies for a set of objects within a specific asset.
93    ///
94    /// This parses object data with TypeTree (when available) and scans for PPtr references
95    /// (`fileID`/`pathID` pairs) to build a dependency graph.
96    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    /// Extract dependencies from a single object by parsing its TypeTree and scanning PPtr-like fields.
175    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            // Prefer a zero-allocation scan that still consumes the object stream
193            // according to the TypeTree. This keeps dependency analysis fast even for
194            // large objects with big buffers/arrays.
195            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                // Fallback: legacy full parse + recursive scan.
199                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    /// Find root objects (objects with no incoming dependencies)
215    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    /// Find leaf objects (objects with no outgoing dependencies)
230    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    /// Detect circular dependencies using DFS
245    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        // Build adjacency list
251        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        // DFS for each unvisited node
260        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    /// DFS helper for cycle detection
278    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                    // Found a cycle
296                    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    /// Clear internal caches
309    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    /// Get cached dependencies for an object
316    pub fn get_cached_dependencies(&self, object_id: i64) -> Option<&Vec<i64>> {
317        self.dependency_cache.get(&object_id)
318    }
319
320    /// Get cached TypeTree-based dependencies for an object within an asset.
321    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        // Unity typetree usually stores { "component": {fileID, pathID} }.
458        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        // Fallback: treat the object itself as PPtr if it matches.
469        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
545/// Relationship analyzer for Unity assets
546///
547/// This struct provides methods for analyzing relationships between
548/// GameObjects, Components, and other Unity objects.
549pub struct RelationshipAnalyzer {
550    /// Cache for GameObject hierarchies
551    hierarchy_cache: HashMap<i64, GameObjectHierarchy>,
552}
553
554impl RelationshipAnalyzer {
555    /// Create a new relationship analyzer
556    pub fn new() -> Self {
557        Self {
558            hierarchy_cache: HashMap::new(),
559        }
560    }
561
562    /// Analyze relationships for a set of objects
563    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        // Separate objects by type
572        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        // Analyze GameObject hierarchy (simplified for now)
589        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        // Analyze component relationships
603        for comp in components {
604            if let Ok(relationship) = self.analyze_component_relationship(comp) {
605                component_relationships.push(relationship);
606            }
607        }
608
609        // Analyze asset references
610        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    /// Analyze relationships for a set of objects within a specific asset.
624    ///
625    /// This method parses GameObject/Transform data via TypeTree (when available) to build:
626    /// - GameObject hierarchy (parent/children/depth)
627    /// - Component relationships (GameObject -> Component)
628    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        // Parse GameObject -> components
670        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                // Heuristic: the Transform component (class_id=4) is the GameObject's Transform.
689                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        // Parse Transform -> (gameobject, parent, children)
703        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        // Build GameObject hierarchy entries
724        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        // Compute depth (BFS from roots)
771        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        // Build component relationships
800        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        // We still keep asset references as placeholder for now.
819        Ok(AssetRelationships {
820            gameobject_hierarchy: hierarchies.into_values().collect(),
821            component_relationships,
822            asset_references: Vec::new(),
823        })
824    }
825
826    /// Analyze GameObject hierarchy (simplified implementation)
827    #[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        // TODO: Implement proper GameObject hierarchy analysis
834        // This would require parsing the GameObject's serialized data
835
836        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, // TODO: Find associated Transform
842            components: Vec::new(),
843            depth: 0,
844        })
845    }
846
847    /// Analyze component relationship (simplified implementation)
848    fn analyze_component_relationship(
849        &self,
850        component: &crate::asset::ObjectInfo,
851    ) -> Result<ComponentRelationship> {
852        // TODO: Implement proper component relationship analysis
853
854        Ok(ComponentRelationship {
855            component_id: component.path_id,
856            component_type: self.get_component_type_name(component.type_id),
857            gameobject_id: 0, // TODO: Find associated GameObject
858            dependencies: Vec::new(),
859            external_dependencies: Vec::new(),
860        })
861    }
862
863    /// Analyze asset reference (simplified implementation)
864    fn analyze_asset_reference(&self, asset: &crate::asset::ObjectInfo) -> Result<AssetReference> {
865        // TODO: Implement proper asset reference analysis
866
867        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    /// Get component type name from type ID
876    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    /// Get asset type name from type ID
885    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    /// Clear internal caches
897    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        // Internal reference: fileID=0
944        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        // External reference: fileID=2
949        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        // GameObject: m_Component = [{component:{fileID:0,pathID:10}}, {component:{fileID:0,pathID:11}}]
973        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        // Transform links: m_GameObject/m_Father/m_Children
994        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}