Files
aibis-dream/Assets/Editor/TimelineNameCollector/YamlTimelineParser.cs
T

376 lines
12 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 组件,提取 timelineName 和 GameObject 路径。
/// 解析方式:按 "--- !u!<classId> &<fileId>" 分段,构建 GameObject / Transform / MonoBehaviour 的索引。
/// </summary>
public static class YamlTimelineParser
{
// 来自 Assets/Scripts/SceneManagement/TimelineKit/DirectorHandler.cs.meta
public const string DirectorHandlerScriptGuid = "07d657d6b80509b4eb04f59afaa9aa2d";
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<TimelineNameCacheEntry> ParseAsset(
string assetPath,
string guid,
string fileHash,
string lastModifiedUtc)
{
var result = new List<TimelineNameCacheEntry>();
if (string.IsNullOrEmpty(assetPath))
return result;
var fullPath = TimelineNameCache.GetAssetFullPath(assetPath);
if (string.IsNullOrEmpty(fullPath) || !File.Exists(fullPath))
return result;
string[] lines;
try
{
lines = File.ReadAllLines(fullPath);
}
catch (Exception e)
{
Debug.LogError($"[YamlTimelineParser] Failed to read '{fullPath}': {e}");
return result;
}
if (lines.Length == 0 || !lines[0].StartsWith("%YAML", StringComparison.Ordinal))
{
// 非 text 序列化,无法解析
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))
continue;
var monoInfo = ExtractDirectorHandlerInfo(
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 TimelineNameCacheEntry 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;
int timelineNameLine = -1;
for (var i = obj.startLine; i <= obj.endLine; i++)
{
var rawLine = lines[i];
var line = rawLine.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);
timelineNameLine = i + 1; // 转为 1-based
}
}
}
if (string.IsNullOrEmpty(timelineName))
{
// 没有设置 timelineName 的组件不计入
return null;
}
var isInScene = assetPath.EndsWith(".unity", StringComparison.OrdinalIgnoreCase);
var goPath = BuildGameObjectPath(gameObjectFileId, gameObjectNames, transforms, transformByGameObject);
return new TimelineNameCacheEntry
{
guid = guid,
assetPath = assetPath,
fileHash = fileHash,
lastModified = lastModifiedUtc,
timelineName = timelineName,
gameObjectPath = goPath,
isInScene = isInScene,
lineNumber = timelineNameLine > 0 ? timelineNameLine : 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);
}
/// <summary>
/// Unity YAML 中的字符串可能将非 ASCII 字符存储为 \uXXXX 转义,需要解码为实际 Unicode 字符。
/// </summary>
private static string UnescapeUnityYamlString(string value)
{
if (string.IsNullOrEmpty(value)) return value;
try
{
return Regex.Unescape(value);
}
catch
{
return value;
}
}
}
}