using System.Linq; using Newtonsoft.Json.Linq; using System.Collections.Generic; using AssetBank.Editor.SchemaConverter.Data.Input; using AssetBank.Editor.SchemaConverter.Data.Output; namespace AssetBank.Editor.SchemaConverter.Processors { /// /// Handles the logic for building the scene hierarchy from GameObject nodes. /// public class HierarchyProcessor { private readonly List _warnings = new(); private IReadOnlyList _allNodes; private Dictionary _unappendedPrefabs; private HashSet _visitedGameObjects; private Dictionary _nodeMap; private static readonly HashSet s_RedundantTransformKeys = new() { "m_Father", "m_Children", "m_PrefabInstance", "m_CorrespondingSourceObject", "m_PrefabAsset" }; public (List rootNodes, List warnings, HashSet consumedAnchorIds) Process( IReadOnlyList allNodes, IReadOnlyDictionary prefabInstances, List prefabGuids) { _allNodes = allNodes; _visitedGameObjects = new HashSet(); _nodeMap = new Dictionary(); var consumedAnchorIds = new HashSet(); var rootHierarchicalNodes = new List(); var allGameObjects = allNodes.Where(n => n.type_id == "1").ToList(); // 1. Identify all Transforms that are linked to a PrefabInstance and create a lookup for them. var allTransforms = allNodes.Where(n => n.type_id == "4").ToList(); _unappendedPrefabs = new Dictionary(); foreach (var transformNode in allTransforms) { var prefabInstanceId = transformNode.data?["Transform"]?["m_PrefabInstance"]?["fileID"]?.ToString(); if (!string.IsNullOrEmpty(prefabInstanceId) && prefabInstances.TryGetValue(prefabInstanceId, out var prefabInstance)) { var guid = prefabInstance.data?["PrefabInstance"]?["m_SourcePrefab"]?["guid"]?.ToString(); var guidIndex = prefabGuids.IndexOf(guid); if (guidIndex != -1) { if (!_nodeMap.TryGetValue(transformNode.anchor_id, out var prefabNode)) { prefabNode = new HierarchicalNode { anchor_id = transformNode.anchor_id, type_id = transformNode.type_id, }; _nodeMap[transformNode.anchor_id] = prefabNode; } prefabNode.prefab_guid_index = (ulong)guidIndex; PopulateNodeFields(prefabNode, transformNode); _unappendedPrefabs[transformNode.anchor_id] = prefabNode; consumedAnchorIds.Add(transformNode.anchor_id); } else if (!string.IsNullOrEmpty(guid)) { _warnings.Add($"Found prefab with GUID '{guid}' but it was not in the provided GUID list."); } } } // 2. Identify and create nodes for all explicit root GameObjects. foreach (var goNode in allGameObjects) { consumedAnchorIds.Add(goNode.anchor_id); var transformComponent = GetTransformComponent(goNode); var fatherId = GetFatherIdFromTransform(transformComponent); if (fatherId == "0") { if (!_nodeMap.TryGetValue(goNode.anchor_id, out var rootNode)) { rootNode = new HierarchicalNode { anchor_id = goNode.anchor_id, type_id = goNode.type_id }; PopulateNodeFields(rootNode, goNode); _nodeMap[goNode.anchor_id] = rootNode; } rootHierarchicalNodes.Add(rootNode); _visitedGameObjects.Add(rootNode.anchor_id); } } // 3. Recursively build the hierarchy for each root GameObject. // Create a copy for iteration as the collection will be modified. foreach (var rootNode in rootHierarchicalNodes.ToList()) { BuildHierarchyRecursive(rootNode.anchor_id, allGameObjects); } // 4. Iteratively process all unvisited GameObjects. bool madeChangesInPass; do { madeChangesInPass = false; var unvisitedGameObjects = allGameObjects.Where(go => !_visitedGameObjects.Contains(go.anchor_id)).ToList(); foreach (var goNode in unvisitedGameObjects) { var transformComponent = GetTransformComponent(goNode); var fatherId = GetFatherIdFromTransform(transformComponent); var parentNode = FindParentNode(fatherId); if (parentNode != null) { if (!_nodeMap.TryGetValue(goNode.anchor_id, out var childNode)) { childNode = new HierarchicalNode { anchor_id = goNode.anchor_id, type_id = goNode.type_id }; PopulateNodeFields(childNode, goNode); _nodeMap[childNode.anchor_id] = childNode; } if (!parentNode.children.Contains(childNode)) { parentNode.children.Add(childNode); } _visitedGameObjects.Add(childNode.anchor_id); BuildHierarchyRecursive(childNode.anchor_id, allGameObjects); madeChangesInPass = true; } } } while (madeChangesInPass); // 5. Any remaining unvisited nodes are true orphans; treat them as roots. var finalUnvisited = allGameObjects.Where(go => !_visitedGameObjects.Contains(go.anchor_id)).ToList(); foreach (var goNode in finalUnvisited) { if (!_nodeMap.TryGetValue(goNode.anchor_id, out var rootNode)) { rootNode = new HierarchicalNode { anchor_id = goNode.anchor_id, type_id = goNode.type_id }; PopulateNodeFields(rootNode, goNode); _nodeMap[rootNode.anchor_id] = rootNode; } rootNode.is_orphan = true; rootHierarchicalNodes.Add(rootNode); _visitedGameObjects.Add(rootNode.anchor_id); _warnings.Add($"GameObject '{goNode.anchor_id}' was treated as an orphan root."); BuildHierarchyRecursive(rootNode.anchor_id, allGameObjects); } // 5b. Prune false roots: remove any nodes from the root list that are actually children of other nodes. var allChildren = new HashSet(); var queue = new Queue(rootHierarchicalNodes); var visitedForPruning = new HashSet(); while (queue.Count > 0) { var node = queue.Dequeue(); if (!visitedForPruning.Add(node)) continue; foreach (var child in node.children) { allChildren.Add(child); queue.Enqueue(child); } } rootHierarchicalNodes.RemoveAll(node => allChildren.Contains(node)); // 6. Update nodes that are implicitly prefabs. UpdateImplicitPrefabInstances(rootHierarchicalNodes, prefabInstances, prefabGuids); // 7. Final Cleanup Pass CleanupNodes(rootHierarchicalNodes); return (rootHierarchicalNodes, _warnings, consumedAnchorIds); } private void BuildHierarchyRecursive(string parentAnchorId, IReadOnlyList allGameObjects) { if (!_nodeMap.TryGetValue(parentAnchorId, out var parentNode)) return; var parentOriginalNode = _allNodes.FirstOrDefault(n => n.anchor_id == parentAnchorId); if (parentOriginalNode == null) return; var parentTransformToken = GetTransformComponent(parentOriginalNode); if (parentOriginalNode.type_id == "4") { parentTransformToken = parentOriginalNode.data; } var childrenIds = parentTransformToken?["Transform"]?["m_Children"]? .Select(c => c["fileID"]?.ToString()) .Where(id => !string.IsNullOrEmpty(id)) .ToList(); if (childrenIds == null) return; foreach (var childId in childrenIds) { var childOriginalNode = allGameObjects.FirstOrDefault(go => GetTransformId(go) == childId); if (childOriginalNode != null) { if (!_nodeMap.TryGetValue(childOriginalNode.anchor_id, out var childNode)) { childNode = new HierarchicalNode { anchor_id = childOriginalNode.anchor_id, type_id = childOriginalNode.type_id }; PopulateNodeFields(childNode, childOriginalNode); _nodeMap[childNode.anchor_id] = childNode; } if (!parentNode.children.Contains(childNode)) { parentNode.children.Add(childNode); } if (_visitedGameObjects.Add(childNode.anchor_id)) { BuildHierarchyRecursive(childNode.anchor_id, allGameObjects); } } else if (_unappendedPrefabs.TryGetValue(childId, out var prefabChildNode)) { if (!parentNode.children.Contains(prefabChildNode)) { parentNode.children.Add(prefabChildNode); } _unappendedPrefabs.Remove(childId); BuildHierarchyRecursive(prefabChildNode.anchor_id, allGameObjects); } } } private static void UpdateImplicitPrefabInstances( IEnumerable nodes, IReadOnlyDictionary prefabInstances, List prefabGuids) { if (nodes == null) return; foreach (var node in nodes) { // Only try to update the node if its prefab status is unknown. if (!node.prefab_guid_index.HasValue) { string prefabInstanceFileId = null; if (node.type_id == "1" && node.Fields.TryGetValue("m_Component", out var componentsToken) && componentsToken is JArray components) { foreach (var component in components) { var fileID = component?["Transform"]?["m_PrefabInstance"]?["fileID"]?.ToString(); if (!string.IsNullOrEmpty(fileID) && fileID != "0") { prefabInstanceFileId = fileID; break; } } } if (prefabInstanceFileId != null) { if (prefabInstances.TryGetValue(prefabInstanceFileId, out var prefabInstanceNode)) { var guid = prefabInstanceNode.data?["PrefabInstance"]?["m_SourcePrefab"]?["guid"]?.ToString(); if (!string.IsNullOrEmpty(guid)) { var guidIndex = prefabGuids.IndexOf(guid); if (guidIndex != -1) { node.prefab_guid_index = (ulong)guidIndex; } } } } } // ALWAYS recurse, regardless of the parent's status. if (node.children.Any()) { UpdateImplicitPrefabInstances(node.children, prefabInstances, prefabGuids); } } } private static void PopulateNodeFields(HierarchicalNode node, OriginalNode originalNode) { JObject dataBlock = null; if (originalNode.type_id == "1") { dataBlock = originalNode.data?["GameObject"] as JObject; } else if (originalNode.type_id == "4") { dataBlock = originalNode.data; } if (dataBlock == null) return; foreach (var property in dataBlock.Properties()) { node.Fields[property.Name] = property.Value; } } private static void CleanupNodes(IEnumerable nodes) { foreach (var node in nodes) { if (node.Fields.TryGetValue("Transform", out var transformToken) && transformToken is JObject transformObj) { foreach (var key in s_RedundantTransformKeys) { transformObj.Remove(key); } } if (node.Fields.TryGetValue("m_Component", out var componentsToken) && componentsToken is JArray components) { foreach (var component in components) { if (component["Transform"] is JObject cTransform) { foreach (var key in s_RedundantTransformKeys) { cTransform.Remove(key); } } } } if (node.children.Any()) { CleanupNodes(node.children); } } } private static JToken GetTransformComponent(OriginalNode goNode) { return goNode.data?["GameObject"]?["m_Component"]?.FirstOrDefault(c => c["Transform"] != null); } private static string GetTransformId(OriginalNode goNode) { return GetTransformComponent(goNode)?["anchor_id"]?.ToString(); } private static string GetFatherIdFromTransform(JToken transformComponent) { return transformComponent?["Transform"]?["m_Father"]?["fileID"]?.ToString() ?? "0"; } private HierarchicalNode FindParentNode(string fatherTransformId) { foreach (var node in _nodeMap.Values) { if (node.type_id == "1") { var originalNode = _allNodes.FirstOrDefault(n => n.anchor_id == node.anchor_id); if (originalNode == null) continue; var transformId = GetTransformId(originalNode); if (transformId == fatherTransformId) { return node; } } else if (node.type_id == "4") { if (node.anchor_id == fatherTransformId) { return node; } } } return null; } } }