376 lines
12 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|