using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using UnityEditor; using UnityEngine; namespace AibisDream.EditorTools { /// /// 解析 Unity YAML 场景 / 预制体中的 DirectorHandler、AnimatorHandler,提取注册名与 GameObject 路径。 /// public static class YamlHandlerNameParser { // DirectorHandler.cs.meta public const string DirectorHandlerScriptGuid = "07d657d6b80509b4eb04f59afaa9aa2d"; // AnimatorHandler.cs.meta public const string AnimatorHandlerScriptGuid = "babec43795065b244ac4cef4af7fa225"; private const int ClassIdGameObject = 1; private const int ClassIdTransform = 4; private const int ClassIdMonoBehaviour = 114; private static readonly Regex HeaderRegex = new Regex(@"^--- !u!(\d+) &(\d+)", RegexOptions.Compiled); private static readonly Regex FileIdRegex = new Regex(@"fileID:\s*(\d+)", RegexOptions.Compiled); private class YamlObject { public int classId; public long fileId; public int startLine; public int endLine; } private class TransformInfo { public long transformFileId; public long parentTransformFileId; public long gameObjectFileId; } public static List ParseAsset( string assetPath, string guid, string fileHash, string lastModifiedUtc) { var result = new List(); if (string.IsNullOrEmpty(assetPath)) return result; var fullPath = HandlerNameCache.GetAssetFullPath(assetPath); if (string.IsNullOrEmpty(fullPath) || !File.Exists(fullPath)) return result; string[] lines; try { lines = File.ReadAllLines(fullPath); } catch (Exception e) { Debug.LogError($"[YamlHandlerNameParser] Failed to read '{fullPath}': {e}"); return result; } if (lines.Length == 0 || !lines[0].StartsWith("%YAML", StringComparison.Ordinal)) return result; var objects = BuildObjects(lines); if (objects.Count == 0) return result; var gameObjectNames = new Dictionary(); var transforms = new Dictionary(); var transformByGameObject = new Dictionary(); foreach (var obj in objects) { switch (obj.classId) { case ClassIdGameObject: var name = ExtractGameObjectName(lines, obj); if (!string.IsNullOrEmpty(name)) gameObjectNames[obj.fileId] = name; break; case ClassIdTransform: var tInfo = ExtractTransformInfo(lines, obj); if (tInfo != null) { transforms[tInfo.transformFileId] = tInfo; if (tInfo.gameObjectFileId != 0 && !transformByGameObject.ContainsKey(tInfo.gameObjectFileId)) transformByGameObject[tInfo.gameObjectFileId] = tInfo.transformFileId; } break; } } foreach (var obj in objects) { if (obj.classId != ClassIdMonoBehaviour) continue; if (IsDirectorHandler(lines, obj)) { var monoInfo = ExtractDirectorHandlerInfo( lines, obj, assetPath, guid, fileHash, lastModifiedUtc, gameObjectNames, transforms, transformByGameObject); if (monoInfo != null) result.Add(monoInfo); } else if (IsAnimatorHandler(lines, obj)) { var monoInfo = ExtractAnimatorHandlerInfo( lines, obj, assetPath, guid, fileHash, lastModifiedUtc, gameObjectNames, transforms, transformByGameObject); if (monoInfo != null) result.Add(monoInfo); } } return result; } private static List BuildObjects(string[] lines) { var objects = new List(); YamlObject current = null; for (var i = 0; i < lines.Length; i++) { var line = lines[i]; var match = HeaderRegex.Match(line); if (!match.Success) continue; if (current != null) { current.endLine = i - 1; objects.Add(current); } if (!int.TryParse(match.Groups[1].Value, out var classId)) continue; if (!long.TryParse(match.Groups[2].Value, out var fileId)) continue; current = new YamlObject { classId = classId, fileId = fileId, startLine = i, endLine = i }; } if (current != null) { current.endLine = lines.Length - 1; objects.Add(current); } return objects; } private static string ExtractGameObjectName(string[] lines, YamlObject obj) { for (var i = obj.startLine; i <= obj.endLine; i++) { var line = lines[i]; var trimmed = line.TrimStart(); if (!trimmed.StartsWith("m_Name:", StringComparison.Ordinal)) continue; var idx = trimmed.IndexOf(':'); if (idx < 0 || idx + 1 >= trimmed.Length) break; var name = trimmed.Substring(idx + 1).Trim(); name = name.Trim('"'); return UnescapeUnityYamlString(name); } return null; } private static TransformInfo ExtractTransformInfo(string[] lines, YamlObject obj) { long gameObjectFileId = 0; long parentTransformFileId = 0; for (var i = obj.startLine; i <= obj.endLine; i++) { var line = lines[i].TrimStart(); if (line.StartsWith("m_GameObject:", StringComparison.Ordinal)) { var match = FileIdRegex.Match(line); if (match.Success && long.TryParse(match.Groups[1].Value, out var id)) gameObjectFileId = id; } else if (line.StartsWith("m_Father:", StringComparison.Ordinal)) { var match = FileIdRegex.Match(line); if (match.Success && long.TryParse(match.Groups[1].Value, out var id)) parentTransformFileId = id; } } if (gameObjectFileId == 0) return null; return new TransformInfo { transformFileId = obj.fileId, parentTransformFileId = parentTransformFileId, gameObjectFileId = gameObjectFileId }; } private static bool IsDirectorHandler(string[] lines, YamlObject obj) { for (var i = obj.startLine; i <= obj.endLine; i++) { var line = lines[i].TrimStart(); if (!line.StartsWith("m_Script:", StringComparison.Ordinal)) continue; if (line.Contains(DirectorHandlerScriptGuid, StringComparison.OrdinalIgnoreCase)) return true; } return false; } private static bool IsAnimatorHandler(string[] lines, YamlObject obj) { for (var i = obj.startLine; i <= obj.endLine; i++) { var line = lines[i].TrimStart(); if (!line.StartsWith("m_Script:", StringComparison.Ordinal)) continue; if (line.Contains(AnimatorHandlerScriptGuid, StringComparison.OrdinalIgnoreCase)) return true; } return false; } private static HandlerNameCacheEntry ExtractDirectorHandlerInfo( string[] lines, YamlObject obj, string assetPath, string guid, string fileHash, string lastModifiedUtc, Dictionary gameObjectNames, Dictionary transforms, Dictionary transformByGameObject) { long gameObjectFileId = 0; string timelineName = null; var nameLine = -1; for (var i = obj.startLine; i <= obj.endLine; i++) { var line = lines[i].TrimStart(); if (line.StartsWith("m_GameObject:", StringComparison.Ordinal)) { var match = FileIdRegex.Match(line); if (match.Success && long.TryParse(match.Groups[1].Value, out var id)) gameObjectFileId = id; } else if (line.StartsWith("timelineName:", StringComparison.Ordinal)) { var idx = line.IndexOf(':'); if (idx >= 0 && idx + 1 < line.Length) { var value = line.Substring(idx + 1).Trim(); value = value.Trim('"'); value = value.Trim('\''); timelineName = UnescapeUnityYamlString(value); nameLine = i + 1; } } } if (string.IsNullOrEmpty(timelineName)) return null; var isInScene = assetPath.EndsWith(".unity", StringComparison.OrdinalIgnoreCase); var goPath = BuildGameObjectPath(gameObjectFileId, gameObjectNames, transforms, transformByGameObject); return new HandlerNameCacheEntry { guid = guid, assetPath = assetPath, fileHash = fileHash, lastModified = lastModifiedUtc, registeredName = timelineName, kind = HandlerEntryKind.DirectorHandler, gameObjectPath = goPath, isInScene = isInScene, lineNumber = nameLine > 0 ? nameLine : obj.startLine + 1 }; } private static HandlerNameCacheEntry ExtractAnimatorHandlerInfo( string[] lines, YamlObject obj, string assetPath, string guid, string fileHash, string lastModifiedUtc, Dictionary gameObjectNames, Dictionary transforms, Dictionary transformByGameObject) { long gameObjectFileId = 0; string animatorName = null; var nameLine = -1; for (var i = obj.startLine; i <= obj.endLine; i++) { var line = lines[i].TrimStart(); if (line.StartsWith("m_GameObject:", StringComparison.Ordinal)) { var match = FileIdRegex.Match(line); if (match.Success && long.TryParse(match.Groups[1].Value, out var id)) gameObjectFileId = id; } else if (line.StartsWith("animatorName:", StringComparison.Ordinal)) { var idx = line.IndexOf(':'); if (idx >= 0 && idx + 1 < line.Length) { var value = line.Substring(idx + 1).Trim(); value = value.Trim('"'); value = value.Trim('\''); animatorName = UnescapeUnityYamlString(value); nameLine = i + 1; } } } if (string.IsNullOrEmpty(animatorName)) return null; var isInScene = assetPath.EndsWith(".unity", StringComparison.OrdinalIgnoreCase); var goPath = BuildGameObjectPath(gameObjectFileId, gameObjectNames, transforms, transformByGameObject); return new HandlerNameCacheEntry { guid = guid, assetPath = assetPath, fileHash = fileHash, lastModified = lastModifiedUtc, registeredName = animatorName, kind = HandlerEntryKind.AnimatorHandler, gameObjectPath = goPath, isInScene = isInScene, lineNumber = nameLine > 0 ? nameLine : obj.startLine + 1 }; } private static string BuildGameObjectPath( long gameObjectFileId, Dictionary gameObjectNames, Dictionary transforms, Dictionary transformByGameObject) { if (gameObjectFileId == 0) return string.Empty; if (!transformByGameObject.TryGetValue(gameObjectFileId, out var transformId)) { gameObjectNames.TryGetValue(gameObjectFileId, out var nameOnly); return nameOnly ?? string.Empty; } var segments = new List(); var currentTransformId = transformId; var safety = 0; while (currentTransformId != 0 && safety++ < 256) { if (!transforms.TryGetValue(currentTransformId, out var tInfo)) break; if (!gameObjectNames.TryGetValue(tInfo.gameObjectFileId, out var name)) name = "GameObject"; segments.Add(name); currentTransformId = tInfo.parentTransformFileId; } segments.Reverse(); return string.Join("/", segments); } private static string UnescapeUnityYamlString(string value) { if (string.IsNullOrEmpty(value)) return value; try { return Regex.Unescape(value); } catch { return value; } } } }