using System; using System.Collections.Generic; using System.IO; using System.Security.Cryptography; using System.Text; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; namespace AibisDream.EditorTools { public enum HandlerEntryKind { DirectorHandler = 0, AnimatorHandler = 1 } [Serializable] public class HandlerNameCacheEntry { public string guid; public string assetPath; public string fileHash; public string lastModified; public string registeredName; public HandlerEntryKind kind; public string gameObjectPath; public bool isInScene; public int lineNumber; } [Serializable] public class HandlerNameCache { public const int CurrentVersion = 2; public int version = CurrentVersion; public string lastFullScanTime; public List entries = new List(); private static readonly string CacheFileName = "handler_name_cache.json"; public static string CacheFolderFullPath => Path.Combine(Application.dataPath, "Editor/HandlerNameCollector/Cache"); public static string CacheFileFullPath => Path.Combine(CacheFolderFullPath, CacheFileName); /// 迁移用:旧版 TimelineNameCollector 缓存路径。 private static string LegacyTimelineCacheFileFullPath => Path.Combine(Application.dataPath, "Editor/TimelineNameCollector/Cache/timeline_name_cache.json"); public static HandlerNameCache Load() { try { string json = null; if (File.Exists(CacheFileFullPath)) json = File.ReadAllText(CacheFileFullPath, Encoding.UTF8); else if (File.Exists(LegacyTimelineCacheFileFullPath)) json = File.ReadAllText(LegacyTimelineCacheFileFullPath, Encoding.UTF8); if (string.IsNullOrEmpty(json)) { return NewEmpty(); } json = MigrateLegacyCacheJson(json); var cache = JsonConvert.DeserializeObject(json); if (cache == null) cache = NewEmpty(); cache.version = CurrentVersion; cache.entries ??= new List(); return cache; } catch (Exception e) { Debug.LogError($"[HandlerNameCache] Failed to load cache: {e}"); return NewEmpty(); } } private static HandlerNameCache NewEmpty() { return new HandlerNameCache { version = CurrentVersion, lastFullScanTime = null, entries = new List() }; } /// /// 将旧版 timelineName 字段、缺省 kind 等合并为当前 JSON 结构。 /// private static string MigrateLegacyCacheJson(string json) { try { var jo = JObject.Parse(json); if (jo["entries"] is JArray arr) { foreach (var token in arr) { if (token is not JObject item) continue; var reg = item["registeredName"]?.ToString(); if (string.IsNullOrWhiteSpace(reg)) { var legacy = item["timelineName"]?.ToString(); if (!string.IsNullOrEmpty(legacy)) item["registeredName"] = legacy; } item.Remove("timelineName"); if (item["kind"] == null) item["kind"] = (int)HandlerEntryKind.DirectorHandler; } } jo["version"] = CurrentVersion; return jo.ToString(Formatting.None); } catch (Exception e) { Debug.LogWarning($"[HandlerNameCache] JSON migration fallback: {e}"); return json; } } public void Save() { try { if (!Directory.Exists(CacheFolderFullPath)) Directory.CreateDirectory(CacheFolderFullPath); entries ??= new List(); 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($"[HandlerNameCache] 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($"[HandlerNameCache] 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; }); } } }