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 _sortedEntries = new List(); private readonly Dictionary> _entriesByName = new Dictionary>(StringComparer.OrdinalIgnoreCase); private readonly HashSet _conflictNames = new HashSet(StringComparer.OrdinalIgnoreCase); private TimelineNameCacheEntry _selectedEntry; [MenuItem("Tools/Timeline Name Collector")] public static void ShowWindow() { var window = GetWindow("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(); } 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(); _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(sceneGuids); foreach (var g in prefabGuids) allGuids.Add(g); var guidList = new List(allGuids); guidList.Sort(); // 现有文件哈希,按 assetPath 去重 var existingHashes = new Dictionary(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; } } }