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;
}
}
}