Files
aibis-dream/Assets/Editor/HandlerNameCollector/YamlHandlerNameParser.cs
T

442 lines
15 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
namespace AibisDream.EditorTools
{
/// <summary>
/// 解析 Unity YAML 场景 / 预制体中的 DirectorHandler、AnimatorHandler,提取注册名与 GameObject 路径。
/// </summary>
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<HandlerNameCacheEntry> ParseAsset(
string assetPath,
string guid,
string fileHash,
string lastModifiedUtc)
{
var result = new List<HandlerNameCacheEntry>();
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<long, string>();
var transforms = new Dictionary<long, TransformInfo>();
var transformByGameObject = new Dictionary<long, long>();
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<YamlObject> BuildObjects(string[] lines)
{
var objects = new List<YamlObject>();
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<long, string> gameObjectNames,
Dictionary<long, TransformInfo> transforms,
Dictionary<long, long> 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<long, string> gameObjectNames,
Dictionary<long, TransformInfo> transforms,
Dictionary<long, long> 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<long, string> gameObjectNames,
Dictionary<long, TransformInfo> transforms,
Dictionary<long, long> 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<string>();
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;
}
}
}
}