chore: 删除TimelineNameCollector旧工具
This commit is contained in:
@@ -1,184 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.EditorTools
|
||||
{
|
||||
[Serializable]
|
||||
public class TimelineNameCacheEntry
|
||||
{
|
||||
public string guid;
|
||||
public string assetPath;
|
||||
public string fileHash;
|
||||
public string lastModified;
|
||||
public string timelineName;
|
||||
public string gameObjectPath;
|
||||
public bool isInScene;
|
||||
public int lineNumber;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class TimelineNameCache
|
||||
{
|
||||
public const int CurrentVersion = 1;
|
||||
|
||||
public int version = CurrentVersion;
|
||||
public string lastFullScanTime;
|
||||
public List<TimelineNameCacheEntry> entries = new List<TimelineNameCacheEntry>();
|
||||
|
||||
private static readonly string CacheFileName = "timeline_name_cache.json";
|
||||
|
||||
public static string CacheFolderFullPath =>
|
||||
Path.Combine(Application.dataPath, "Editor/TimelineNameCollector/Cache");
|
||||
|
||||
public static string CacheFileFullPath =>
|
||||
Path.Combine(CacheFolderFullPath, CacheFileName);
|
||||
|
||||
public static TimelineNameCache Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(CacheFileFullPath))
|
||||
{
|
||||
return new TimelineNameCache
|
||||
{
|
||||
version = CurrentVersion,
|
||||
lastFullScanTime = null,
|
||||
entries = new List<TimelineNameCacheEntry>()
|
||||
};
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(CacheFileFullPath, Encoding.UTF8);
|
||||
if (string.IsNullOrEmpty(json))
|
||||
{
|
||||
return new TimelineNameCache
|
||||
{
|
||||
version = CurrentVersion,
|
||||
lastFullScanTime = null,
|
||||
entries = new List<TimelineNameCacheEntry>()
|
||||
};
|
||||
}
|
||||
|
||||
var cache = JsonConvert.DeserializeObject<TimelineNameCache>(json);
|
||||
if (cache == null)
|
||||
{
|
||||
cache = new TimelineNameCache();
|
||||
}
|
||||
|
||||
if (cache.version != CurrentVersion)
|
||||
{
|
||||
cache.version = CurrentVersion;
|
||||
cache.lastFullScanTime = null;
|
||||
cache.entries = cache.entries ?? new List<TimelineNameCacheEntry>();
|
||||
}
|
||||
|
||||
return cache;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[TimelineNameCache] Failed to load cache: {e}");
|
||||
return new TimelineNameCache
|
||||
{
|
||||
version = CurrentVersion,
|
||||
lastFullScanTime = null,
|
||||
entries = new List<TimelineNameCacheEntry>()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(CacheFolderFullPath))
|
||||
{
|
||||
Directory.CreateDirectory(CacheFolderFullPath);
|
||||
}
|
||||
|
||||
entries ??= new List<TimelineNameCacheEntry>();
|
||||
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
StringEscapeHandling = StringEscapeHandling.Default
|
||||
};
|
||||
var json = JsonConvert.SerializeObject(this, settings);
|
||||
File.WriteAllText(CacheFileFullPath, json, Encoding.UTF8);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[TimelineNameCache] Failed to save cache: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
public void SetLastFullScanNow()
|
||||
{
|
||||
lastFullScanTime = DateTime.UtcNow.ToString("o");
|
||||
}
|
||||
|
||||
public static string ComputeFileHash(string fullPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using (var stream = File.OpenRead(fullPath))
|
||||
using (var md5 = MD5.Create())
|
||||
{
|
||||
var hash = md5.ComputeHash(stream);
|
||||
var sb = new StringBuilder(hash.Length * 2);
|
||||
foreach (var b in hash)
|
||||
{
|
||||
sb.Append(b.ToString("x2"));
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[TimelineNameCache] Failed to compute file hash for '{fullPath}': {e}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetAssetFullPath(string assetPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assetPath))
|
||||
return null;
|
||||
|
||||
if (!assetPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase) &&
|
||||
!assetPath.Equals("Assets", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var relative = assetPath.Substring("Assets".Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
return Path.Combine(Application.dataPath, relative);
|
||||
}
|
||||
|
||||
public void RemoveEntriesForAsset(string assetPath)
|
||||
{
|
||||
if (entries == null || string.IsNullOrEmpty(assetPath)) return;
|
||||
entries.RemoveAll(e => e != null && e.assetPath == assetPath);
|
||||
}
|
||||
|
||||
public void CleanupDeletedAssets()
|
||||
{
|
||||
if (entries == null) return;
|
||||
entries.RemoveAll(e =>
|
||||
{
|
||||
if (e == null || string.IsNullOrEmpty(e.assetPath)) return true;
|
||||
return AssetDatabase.LoadMainAssetAtPath(e.assetPath) == null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 58e8fb78cff00f44482eff9d6a1b7a64
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,567 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.EditorTools
|
||||
{
|
||||
public class TimelineNameCollectorWindow : EditorWindow
|
||||
{
|
||||
private TimelineNameCache _cache;
|
||||
|
||||
private Vector2 _scrollPos;
|
||||
private string _searchText = string.Empty;
|
||||
private bool _showScenes = true;
|
||||
private bool _showPrefabs = true;
|
||||
private bool _showConflictsOnly;
|
||||
|
||||
private readonly List<TimelineNameCacheEntry> _sortedEntries = new List<TimelineNameCacheEntry>();
|
||||
private readonly Dictionary<string, List<TimelineNameCacheEntry>> _entriesByName =
|
||||
new Dictionary<string, List<TimelineNameCacheEntry>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly HashSet<string> _conflictNames =
|
||||
new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private TimelineNameCacheEntry _selectedEntry;
|
||||
|
||||
[MenuItem("Tools/Timeline Name Collector")]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
var window = GetWindow<TimelineNameCollectorWindow>("Timeline Name Collector");
|
||||
window.minSize = new Vector2(700, 400);
|
||||
window.Show();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
LoadCacheIfNeeded();
|
||||
RebuildIndexes();
|
||||
}
|
||||
|
||||
private void LoadCacheIfNeeded()
|
||||
{
|
||||
_cache ??= TimelineNameCache.Load();
|
||||
_cache.entries ??= new List<TimelineNameCacheEntry>();
|
||||
}
|
||||
|
||||
private void RebuildIndexes()
|
||||
{
|
||||
_sortedEntries.Clear();
|
||||
_entriesByName.Clear();
|
||||
_conflictNames.Clear();
|
||||
|
||||
if (_cache == null || _cache.entries == null)
|
||||
return;
|
||||
|
||||
foreach (var entry in _cache.entries)
|
||||
{
|
||||
if (entry == null)
|
||||
continue;
|
||||
|
||||
_sortedEntries.Add(entry);
|
||||
|
||||
if (!_entriesByName.TryGetValue(entry.timelineName, out var list))
|
||||
{
|
||||
list = new List<TimelineNameCacheEntry>();
|
||||
_entriesByName[entry.timelineName] = list;
|
||||
}
|
||||
list.Add(entry);
|
||||
}
|
||||
|
||||
_sortedEntries.Sort((a, b) =>
|
||||
{
|
||||
var nameCompare = string.Compare(a.timelineName, b.timelineName, StringComparison.OrdinalIgnoreCase);
|
||||
if (nameCompare != 0) return nameCompare;
|
||||
var typeCompare = a.isInScene.CompareTo(b.isInScene);
|
||||
if (typeCompare != 0) return -typeCompare; // 场景优先
|
||||
return string.Compare(a.assetPath, b.assetPath, StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
|
||||
foreach (var kv in _entriesByName)
|
||||
{
|
||||
if (kv.Value.Count > 1)
|
||||
{
|
||||
_conflictNames.Add(kv.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
LoadCacheIfNeeded();
|
||||
|
||||
DrawToolbar();
|
||||
EditorGUILayout.Space();
|
||||
DrawFilterBar();
|
||||
EditorGUILayout.Space();
|
||||
DrawSummary();
|
||||
EditorGUILayout.Space();
|
||||
DrawListArea();
|
||||
EditorGUILayout.Space();
|
||||
DrawDetailArea();
|
||||
}
|
||||
|
||||
private void DrawToolbar()
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
|
||||
|
||||
if (GUILayout.Button("刷新选中", EditorStyles.toolbarButton, GUILayout.Width(90)))
|
||||
{
|
||||
RefreshSelected();
|
||||
}
|
||||
|
||||
if (GUILayout.Button("刷新全部", EditorStyles.toolbarButton, GUILayout.Width(90)))
|
||||
{
|
||||
RefreshAll(forceFull: false);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("强制全量", EditorStyles.toolbarButton, GUILayout.Width(90)))
|
||||
{
|
||||
RefreshAll(forceFull: true);
|
||||
}
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
var lastFullScanLabel = string.IsNullOrEmpty(_cache.lastFullScanTime)
|
||||
? "未进行全量扫描"
|
||||
: $"上次全量: {FormatTime(_cache.lastFullScanTime)}";
|
||||
GUILayout.Label(lastFullScanLabel, EditorStyles.miniLabel);
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private void DrawFilterBar()
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
|
||||
GUILayout.Label("搜索:", GUILayout.Width(40));
|
||||
_searchText = EditorGUILayout.TextField(_searchText);
|
||||
|
||||
_showScenes = GUILayout.Toggle(_showScenes, "场景", GUILayout.Width(60));
|
||||
_showPrefabs = GUILayout.Toggle(_showPrefabs, "预制体", GUILayout.Width(70));
|
||||
_showConflictsOnly = GUILayout.Toggle(_showConflictsOnly, "只看冲突", GUILayout.Width(80));
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private void DrawSummary()
|
||||
{
|
||||
var total = _sortedEntries.Count;
|
||||
var conflictCount = _conflictNames.Count;
|
||||
EditorGUILayout.LabelField($"找到 {total} 个 TimelineName 条目({conflictCount} 个重名)",
|
||||
EditorStyles.boldLabel);
|
||||
}
|
||||
|
||||
private void DrawListArea()
|
||||
{
|
||||
EditorGUILayout.LabelField("列表", EditorStyles.boldLabel);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Label("Timeline Name", GUILayout.Width(220));
|
||||
GUILayout.Label("类型", GUILayout.Width(50));
|
||||
GUILayout.Label("位置", GUILayout.ExpandWidth(true));
|
||||
GUILayout.Label("", GUILayout.Width(90));
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
var rect = GUILayoutUtility.GetRect(0, 100000, 0, 100000);
|
||||
_scrollPos = GUI.BeginScrollView(rect, _scrollPos, new Rect(0, 0, rect.width - 20, _sortedEntries.Count * 20 + 10));
|
||||
|
||||
var y = 0f;
|
||||
var rowHeight = 20f;
|
||||
var viewWidth = rect.width - 20;
|
||||
|
||||
foreach (var entry in _sortedEntries)
|
||||
{
|
||||
if (!PassFilter(entry))
|
||||
continue;
|
||||
|
||||
var isConflict = _conflictNames.Contains(entry.timelineName);
|
||||
var rowRect = new Rect(0, y, viewWidth, rowHeight);
|
||||
DrawRow(rowRect, entry, isConflict);
|
||||
y += rowHeight;
|
||||
}
|
||||
|
||||
GUI.EndScrollView();
|
||||
}
|
||||
|
||||
private void DrawRow(Rect rect, TimelineNameCacheEntry entry, bool isConflict)
|
||||
{
|
||||
var typeLabel = entry.isInScene ? "场景" : "预制体";
|
||||
var location = GetShortLocation(entry);
|
||||
|
||||
var bgColor = GUI.backgroundColor;
|
||||
if (isConflict)
|
||||
{
|
||||
GUI.backgroundColor = new Color(1f, 0.9f, 0.9f);
|
||||
}
|
||||
|
||||
if (Event.current.type == EventType.MouseDown && rect.Contains(Event.current.mousePosition))
|
||||
{
|
||||
if (Event.current.button == 0)
|
||||
{
|
||||
_selectedEntry = entry;
|
||||
Repaint();
|
||||
Event.current.Use();
|
||||
}
|
||||
}
|
||||
|
||||
GUI.Box(rect, GUIContent.none, EditorStyles.helpBox);
|
||||
|
||||
GUI.backgroundColor = bgColor;
|
||||
|
||||
var colRect = rect;
|
||||
colRect.x += 4;
|
||||
colRect.width = 220;
|
||||
GUI.Label(colRect, entry.timelineName);
|
||||
|
||||
colRect.x += colRect.width + 4;
|
||||
colRect.width = 50;
|
||||
GUI.Label(colRect, typeLabel);
|
||||
|
||||
colRect.x += colRect.width + 4;
|
||||
colRect.width = rect.width - colRect.x - 100;
|
||||
GUI.Label(colRect, location);
|
||||
|
||||
colRect.x += colRect.width + 4;
|
||||
colRect.width = 90;
|
||||
if (GUI.Button(colRect, "项目中显示"))
|
||||
{
|
||||
PingEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawDetailArea()
|
||||
{
|
||||
EditorGUILayout.LabelField("选中项详情", EditorStyles.boldLabel);
|
||||
|
||||
if (_selectedEntry == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox("在上方列表中点击某一行查看详情。", MessageType.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
GUILayout.Label($"Timeline Name: {_selectedEntry.timelineName}", EditorStyles.boldLabel);
|
||||
|
||||
if (_entriesByName.TryGetValue(_selectedEntry.timelineName, out var group))
|
||||
{
|
||||
foreach (var e in group)
|
||||
{
|
||||
EditorGUILayout.BeginVertical("box");
|
||||
|
||||
EditorGUILayout.LabelField("资源路径", e.assetPath);
|
||||
EditorGUILayout.LabelField("GameObject", string.IsNullOrEmpty(e.gameObjectPath) ? "(未知)" : e.gameObjectPath);
|
||||
EditorGUILayout.LabelField("类型", e.isInScene ? "场景" : "预制体");
|
||||
EditorGUILayout.LabelField("行号", e.lineNumber.ToString());
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("在Project窗口显示", GUILayout.Width(140)))
|
||||
{
|
||||
PingEntry(e);
|
||||
}
|
||||
|
||||
if (e.isInScene && GUILayout.Button("打开场景并选中", GUILayout.Width(140)))
|
||||
{
|
||||
OpenSceneAndSelect(e);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("复制名称", GUILayout.Width(100)))
|
||||
{
|
||||
EditorGUIUtility.systemCopyBuffer = e.timelineName;
|
||||
}
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
EditorGUILayout.EndVertical();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool PassFilter(TimelineNameCacheEntry entry)
|
||||
{
|
||||
if (!_showScenes && entry.isInScene)
|
||||
return false;
|
||||
if (!_showPrefabs && !entry.isInScene)
|
||||
return false;
|
||||
|
||||
if (_showConflictsOnly && !_conflictNames.Contains(entry.timelineName))
|
||||
return false;
|
||||
|
||||
if (string.IsNullOrEmpty(_searchText))
|
||||
return true;
|
||||
|
||||
var s = _searchText.Trim();
|
||||
if (s.Length == 0)
|
||||
return true;
|
||||
|
||||
var cmp = StringComparison.OrdinalIgnoreCase;
|
||||
|
||||
if (!string.IsNullOrEmpty(entry.timelineName) &&
|
||||
entry.timelineName.Contains(s, cmp))
|
||||
return true;
|
||||
|
||||
if (!string.IsNullOrEmpty(entry.assetPath) &&
|
||||
entry.assetPath.Contains(s, cmp))
|
||||
return true;
|
||||
|
||||
if (!string.IsNullOrEmpty(entry.gameObjectPath) &&
|
||||
entry.gameObjectPath.Contains(s, cmp))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string GetShortLocation(TimelineNameCacheEntry entry)
|
||||
{
|
||||
if (string.IsNullOrEmpty(entry.assetPath))
|
||||
return string.Empty;
|
||||
|
||||
var fileName = Path.GetFileNameWithoutExtension(entry.assetPath);
|
||||
|
||||
if (string.IsNullOrEmpty(entry.gameObjectPath))
|
||||
{
|
||||
return fileName;
|
||||
}
|
||||
|
||||
return $"{fileName} / {entry.gameObjectPath}";
|
||||
}
|
||||
|
||||
private static void PingEntry(TimelineNameCacheEntry entry)
|
||||
{
|
||||
if (string.IsNullOrEmpty(entry.assetPath))
|
||||
return;
|
||||
|
||||
var obj = AssetDatabase.LoadMainAssetAtPath(entry.assetPath);
|
||||
if (obj != null)
|
||||
{
|
||||
EditorGUIUtility.PingObject(obj);
|
||||
Selection.activeObject = obj;
|
||||
}
|
||||
}
|
||||
|
||||
private static void OpenSceneAndSelect(TimelineNameCacheEntry entry)
|
||||
{
|
||||
if (string.IsNullOrEmpty(entry.assetPath))
|
||||
return;
|
||||
|
||||
if (!entry.isInScene)
|
||||
return;
|
||||
|
||||
if (!File.Exists(entry.assetPath))
|
||||
{
|
||||
Debug.LogWarning($"场景文件不存在: {entry.assetPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
var scene = EditorSceneManager.OpenScene(entry.assetPath, OpenSceneMode.Single);
|
||||
if (!scene.IsValid())
|
||||
{
|
||||
Debug.LogWarning($"无法打开场景: {entry.assetPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(entry.gameObjectPath))
|
||||
return;
|
||||
|
||||
var parts = entry.gameObjectPath.Split('/');
|
||||
GameObject current = null;
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (current == null)
|
||||
{
|
||||
var roots = scene.GetRootGameObjects();
|
||||
current = Array.Find(roots, go => go.name == part);
|
||||
}
|
||||
else
|
||||
{
|
||||
var child = current.transform.Find(part);
|
||||
current = child != null ? child.gameObject : null;
|
||||
}
|
||||
|
||||
if (current == null)
|
||||
break;
|
||||
}
|
||||
|
||||
if (current != null)
|
||||
{
|
||||
Selection.activeGameObject = current;
|
||||
EditorGUIUtility.PingObject(current);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"在场景 {scene.name} 中未找到路径: {entry.gameObjectPath}");
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshSelected()
|
||||
{
|
||||
LoadCacheIfNeeded();
|
||||
|
||||
var guids = Selection.assetGUIDs;
|
||||
if (guids == null || guids.Length == 0)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Timeline Name Collector", "请在 Project 窗口中先选中场景或预制体资源。", "确定");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
EditorUtility.DisplayProgressBar("Timeline Name Collector", "正在刷新选中资源...", 0f);
|
||||
|
||||
var processed = 0;
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
var assetPath = AssetDatabase.GUIDToAssetPath(guid);
|
||||
if (!IsSupportedAsset(assetPath))
|
||||
continue;
|
||||
|
||||
ProcessSingleAsset(guid, assetPath);
|
||||
|
||||
processed++;
|
||||
EditorUtility.DisplayProgressBar(
|
||||
"Timeline Name Collector",
|
||||
$"正在解析: {assetPath}",
|
||||
processed / (float)guids.Length);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
}
|
||||
|
||||
_cache.CleanupDeletedAssets();
|
||||
_cache.Save();
|
||||
RebuildIndexes();
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private void RefreshAll(bool forceFull)
|
||||
{
|
||||
LoadCacheIfNeeded();
|
||||
|
||||
var sceneGuids = AssetDatabase.FindAssets("t:Scene");
|
||||
var prefabGuids = AssetDatabase.FindAssets("t:Prefab");
|
||||
|
||||
var allGuids = new HashSet<string>(sceneGuids);
|
||||
foreach (var g in prefabGuids)
|
||||
allGuids.Add(g);
|
||||
|
||||
var guidList = new List<string>(allGuids);
|
||||
guidList.Sort();
|
||||
|
||||
// 现有文件哈希,按 assetPath 去重
|
||||
var existingHashes = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var e in _cache.entries)
|
||||
{
|
||||
if (e == null || string.IsNullOrEmpty(e.assetPath) || string.IsNullOrEmpty(e.fileHash))
|
||||
continue;
|
||||
|
||||
if (!existingHashes.ContainsKey(e.assetPath))
|
||||
{
|
||||
existingHashes[e.assetPath] = e.fileHash;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
EditorUtility.DisplayProgressBar("Timeline Name Collector",
|
||||
forceFull ? "正在强制全量扫描..." : "正在增量扫描...", 0f);
|
||||
|
||||
for (var i = 0; i < guidList.Count; i++)
|
||||
{
|
||||
var guid = guidList[i];
|
||||
var assetPath = AssetDatabase.GUIDToAssetPath(guid);
|
||||
if (!IsSupportedAsset(assetPath))
|
||||
continue;
|
||||
|
||||
var fullPath = TimelineNameCache.GetAssetFullPath(assetPath);
|
||||
if (string.IsNullOrEmpty(fullPath) || !File.Exists(fullPath))
|
||||
continue;
|
||||
|
||||
var newHash = TimelineNameCache.ComputeFileHash(fullPath);
|
||||
|
||||
var needScan = forceFull;
|
||||
if (!needScan)
|
||||
{
|
||||
if (!existingHashes.TryGetValue(assetPath, out var oldHash) ||
|
||||
string.IsNullOrEmpty(newHash) ||
|
||||
!string.Equals(oldHash, newHash, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
needScan = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!needScan)
|
||||
continue;
|
||||
|
||||
ProcessSingleAsset(guid, assetPath, newHash);
|
||||
|
||||
var progress = (i + 1) / (float)guidList.Count;
|
||||
EditorUtility.DisplayProgressBar("Timeline Name Collector",
|
||||
$"正在解析: {assetPath}", progress);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
}
|
||||
|
||||
_cache.CleanupDeletedAssets();
|
||||
_cache.SetLastFullScanNow();
|
||||
_cache.Save();
|
||||
RebuildIndexes();
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private void ProcessSingleAsset(string guid, string assetPath, string precomputedHash = null)
|
||||
{
|
||||
var fullPath = TimelineNameCache.GetAssetFullPath(assetPath);
|
||||
if (string.IsNullOrEmpty(fullPath) || !File.Exists(fullPath))
|
||||
return;
|
||||
|
||||
var fileHash = precomputedHash ?? TimelineNameCache.ComputeFileHash(fullPath);
|
||||
var lastModifiedUtc = File.GetLastWriteTimeUtc(fullPath).ToString("o");
|
||||
|
||||
_cache.RemoveEntriesForAsset(assetPath);
|
||||
|
||||
var entries = YamlTimelineParser.ParseAsset(
|
||||
assetPath,
|
||||
guid,
|
||||
fileHash,
|
||||
lastModifiedUtc);
|
||||
|
||||
if (entries != null && entries.Count > 0)
|
||||
{
|
||||
_cache.entries.AddRange(entries);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsSupportedAsset(string assetPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assetPath))
|
||||
return false;
|
||||
|
||||
var ext = Path.GetExtension(assetPath);
|
||||
if (string.IsNullOrEmpty(ext))
|
||||
return false;
|
||||
|
||||
ext = ext.ToLowerInvariant();
|
||||
return ext == ".unity" || ext == ".prefab";
|
||||
}
|
||||
|
||||
private static string FormatTime(string isoTime)
|
||||
{
|
||||
if (string.IsNullOrEmpty(isoTime))
|
||||
return "-";
|
||||
|
||||
if (DateTime.TryParse(isoTime, out var dt))
|
||||
{
|
||||
return dt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
return isoTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a43798aefb6a4142a7abe8d3dc1649a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,375 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 19f28acc911bf7d4e86035daebc33190
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user