using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using UnityEditor; using UnityEditor.AddressableAssets; using UnityEditor.AddressableAssets.Settings; using UnityEngine; using BaseGames.Core.Assets; namespace BaseGames.Editor { /// /// Addressable 批量注册工具 /// 菜单:BaseGames → Tools → Addressable Batch Tool (Alt+Shift+A) /// /// 三种工作流: /// ① 同步 AddressKeys — 读取 AddressKeys.cs 中的常量,按名称在 Project 中搜索对应资产并自动注册 /// ② 文件夹批量注册 — 拖入文件夹,将其下所有指定类型的资产注册到选定分组,地址格式可配置 /// ③ 选中资产注册 — 在 Project 窗口选中资产后,一键批量注册到选定分组 /// public class AddressableBatchTool : EditorWindow { // ── 常量 ───────────────────────────────────────────────────────────── private const string Title = "Addressable 批量工具"; private const string MenuPath = "BaseGames/Addressables/Addressable Batch Tool"; private const string PrefsKey = "AddressableBatch."; // ── 状态 ───────────────────────────────────────────────────────────── private int _tab; private string[] _tabNames = { "① 同步 AddressKeys", "② 文件夹批量注册", "③ 选中资产注册" }; // Tab ① private List _keyEntries; private Vector2 _keyScrollPos; private bool _onlyShowMissing = true; private bool _autoSearch = true; // 自动按名称搜索资产 private bool _autoGroupByPrefix = true; // 按 Key 前缀自动选/建分组 // Key 前缀 → 分组名称映射(Tab ① 自动分组用) // 规范数据统一来自 AddressableRules,此处不再声明本地副本。 // Tab ② private DefaultAsset _folderAsset; private string _folderPath; private bool _includeSubfolders = true; private AddressFormat _addressFormat = AddressFormat.FileName; private string _addressPrefix = ""; private string[] _assetTypeFilters = { "*.prefab", "*.unity", "*.asset" }; private bool _filterPrefab = true; private bool _filterScene = true; private bool _filterSO = true; private bool _filterTexture; private bool _filterAudio; private List _folderEntries; private Vector2 _folderScrollPos; // Tab ③ private List _selectionEntries; private Vector2 _selectionScrollPos; // 共用 private int _targetGroupIndex; private string[] _groupNames; private string _newGroupName = "New Group"; private string _newLabel = ""; private bool _overwriteAddress; // ── 样式(惰性初始化)──────────────────────────────────────────────── private GUIStyle _headerStyle; private GUIStyle _okStyle; private GUIStyle _warnStyle; private GUIStyle _boldStyle; private bool _stylesInitialized; // ───────────────────────────────────────────────────────────────────── [MenuItem(MenuPath, priority = 200)] [MenuItem("BaseGames/Addressables/Open Addressable Batch Tool", priority = 250)] public static void OpenWindow() { var win = GetWindow(Title); win.minSize = new Vector2(600, 460); win.Show(); } // ══ GUI ══════════════════════════════════════════════════════════════ private void OnGUI() { InitStyles(); if (AddressableAssetSettingsDefaultObject.Settings == null) { EditorGUILayout.HelpBox( "Addressable Settings 未初始化。\n" + "请先执行 Window → Asset Management → Addressables → Groups → Create Addressables Settings。", MessageType.Error); return; } RefreshGroupNames(); EditorGUILayout.Space(4); _tab = GUILayout.Toolbar(_tab, _tabNames, GUILayout.Height(28)); EditorGUILayout.Space(4); switch (_tab) { case 0: DrawSyncTab(); break; case 1: DrawFolderTab(); break; case 2: DrawSelectionTab(); break; } EditorGUILayout.Space(4); DrawSharedOptions(); } // ══ Tab ① 同步 AddressKeys ═══════════════════════════════════════════ private void DrawSyncTab() { EditorGUILayout.LabelField("根据 AddressKeys.cs 中的常量,自动搜索匹配资产并注册到 Addressables。", EditorStyles.wordWrappedMiniLabel); EditorGUILayout.Space(4); using (new EditorGUILayout.HorizontalScope()) { _onlyShowMissing = GUILayout.Toggle(_onlyShowMissing, "仅显示未注册项", GUILayout.Width(140)); _autoSearch = GUILayout.Toggle(_autoSearch, "自动按文件名搜索", GUILayout.Width(140)); _autoGroupByPrefix = GUILayout.Toggle(_autoGroupByPrefix, "按前缀自动分组", GUILayout.Width(120)); GUILayout.FlexibleSpace(); if (GUILayout.Button("刷新列表", GUILayout.Width(80))) RefreshKeyEntries(); if (GUILayout.Button("注册所有已匹配项", GUILayout.Width(120))) RegisterAllMatchedKeys(); } if (_keyEntries == null) RefreshKeyEntries(); EditorGUILayout.Space(4); // 列表表头 using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) { EditorGUILayout.LabelField("常量名", _boldStyle, GUILayout.Width(200)); EditorGUILayout.LabelField("地址 Key", _boldStyle, GUILayout.Width(180)); EditorGUILayout.LabelField("状态", _boldStyle, GUILayout.Width(100)); EditorGUILayout.LabelField("匹配资产", _boldStyle); } var displayList = _onlyShowMissing ? _keyEntries.Where(e => !e.IsRegistered).ToList() : _keyEntries; _keyScrollPos = EditorGUILayout.BeginScrollView(_keyScrollPos, GUILayout.ExpandHeight(true)); foreach (var entry in displayList) { using (new EditorGUILayout.HorizontalScope(GUILayout.Height(20))) { EditorGUILayout.LabelField(entry.FieldName, GUILayout.Width(200)); EditorGUILayout.LabelField(entry.AddressKey, GUILayout.Width(180)); if (entry.IsRegistered) { EditorGUILayout.LabelField("✅ 已注册", _okStyle, GUILayout.Width(100)); EditorGUILayout.LabelField(entry.ExistingAssetPath ?? "—"); } else if (entry.FoundAssetPath != null) { EditorGUILayout.LabelField("⚠ 未注册", _warnStyle, GUILayout.Width(100)); EditorGUILayout.LabelField(entry.FoundAssetPath, GUILayout.ExpandWidth(true)); if (GUILayout.Button("注册", GUILayout.Width(50))) RegisterKeyEntry(entry); } else { EditorGUILayout.LabelField("❌ 未找到", _warnStyle, GUILayout.Width(100)); entry.ManualAsset = (UnityEngine.Object)EditorGUILayout.ObjectField( entry.ManualAsset, typeof(UnityEngine.Object), false); if (entry.ManualAsset != null) { if (GUILayout.Button("注册", GUILayout.Width(50))) RegisterKeyEntryManual(entry); } } } } EditorGUILayout.EndScrollView(); EditorGUILayout.Space(4); var total = _keyEntries.Count; var registered = _keyEntries.Count(e => e.IsRegistered); var matched = _keyEntries.Count(e => !e.IsRegistered && e.FoundAssetPath != null); EditorGUILayout.LabelField( $"总计 {total} 个 Key | 已注册 {registered} | 已搜索到但未注册 {matched} | 未找到 {total - registered - matched}", EditorStyles.miniLabel); } // ══ Tab ② 文件夹批量注册 ═════════════════════════════════════════════ private void DrawFolderTab() { EditorGUILayout.LabelField("将指定文件夹中所有符合条件的资产批量注册到 Addressables。", EditorStyles.wordWrappedMiniLabel); EditorGUILayout.Space(4); // 文件夹选择 using (new EditorGUILayout.HorizontalScope()) { _folderAsset = (DefaultAsset)EditorGUILayout.ObjectField( "目标文件夹", _folderAsset, typeof(DefaultAsset), false); if (_folderAsset != null) _folderPath = AssetDatabase.GetAssetPath(_folderAsset); } if (!string.IsNullOrEmpty(_folderPath) && !AssetDatabase.IsValidFolder(_folderPath)) { EditorGUILayout.HelpBox("请拖入一个文件夹(蓝色图标),不是文件。", MessageType.Warning); _folderPath = null; } _includeSubfolders = EditorGUILayout.Toggle("包含子文件夹", _includeSubfolders); // 资产类型筛选 EditorGUILayout.LabelField("资产类型筛选", EditorStyles.boldLabel); using (new EditorGUILayout.HorizontalScope()) { _filterPrefab = GUILayout.Toggle(_filterPrefab, "Prefab", GUILayout.Width(70)); _filterScene = GUILayout.Toggle(_filterScene, "Scene", GUILayout.Width(70)); _filterSO = GUILayout.Toggle(_filterSO, "SO/Asset", GUILayout.Width(80)); _filterTexture = GUILayout.Toggle(_filterTexture, "Texture", GUILayout.Width(70)); _filterAudio = GUILayout.Toggle(_filterAudio, "Audio", GUILayout.Width(70)); } // 地址格式 _addressFormat = (AddressFormat)EditorGUILayout.EnumPopup("地址格式", _addressFormat); if (_addressFormat == AddressFormat.PrefixPlusFileName || _addressFormat == AddressFormat.PrefixPlusRelativePath) { _addressPrefix = EditorGUILayout.TextField("地址前缀", _addressPrefix); } EditorGUILayout.Space(4); using (new EditorGUILayout.HorizontalScope()) { GUILayout.FlexibleSpace(); GUI.enabled = !string.IsNullOrEmpty(_folderPath); if (GUILayout.Button("扫描文件夹", GUILayout.Width(100))) ScanFolder(); GUI.enabled = _folderEntries != null && _folderEntries.Count > 0; if (GUILayout.Button("注册所有", GUILayout.Width(100))) RegisterAllFolderEntries(); GUI.enabled = true; } if (_folderEntries == null || _folderEntries.Count == 0) { EditorGUILayout.HelpBox("拖入文件夹后点击「扫描文件夹」。", MessageType.Info); return; } EditorGUILayout.Space(4); using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) { EditorGUILayout.LabelField("资产路径", _boldStyle, GUILayout.ExpandWidth(true)); EditorGUILayout.LabelField("预计地址", _boldStyle, GUILayout.Width(200)); EditorGUILayout.LabelField("状态", _boldStyle, GUILayout.Width(80)); } _folderScrollPos = EditorGUILayout.BeginScrollView(_folderScrollPos, GUILayout.ExpandHeight(true)); foreach (var entry in _folderEntries) { using (new EditorGUILayout.HorizontalScope(GUILayout.Height(18))) { EditorGUILayout.LabelField(entry.AssetPath, GUILayout.ExpandWidth(true)); entry.Address = EditorGUILayout.TextField(entry.Address, GUILayout.Width(200)); var label = entry.AlreadyRegistered ? "✅ 已有" : "待注册"; var style = entry.AlreadyRegistered ? _okStyle : EditorStyles.miniLabel; EditorGUILayout.LabelField(label, style, GUILayout.Width(80)); } } EditorGUILayout.EndScrollView(); int newCount = _folderEntries.Count(e => !e.AlreadyRegistered); EditorGUILayout.LabelField($"共 {_folderEntries.Count} 个资产,{newCount} 个待注册", EditorStyles.miniLabel); } // ══ Tab ③ 选中资产注册 ════════════════════════════════════════════════ private void DrawSelectionTab() { EditorGUILayout.LabelField("在 Project 窗口中选中资产或文件夹,然后点击「读取选中项」。", EditorStyles.wordWrappedMiniLabel); EditorGUILayout.Space(4); using (new EditorGUILayout.HorizontalScope()) { GUILayout.FlexibleSpace(); if (GUILayout.Button("读取选中项", GUILayout.Width(110))) LoadSelection(); GUI.enabled = _selectionEntries != null && _selectionEntries.Count > 0; if (GUILayout.Button("注册所有", GUILayout.Width(100))) RegisterAllSelectionEntries(); GUI.enabled = true; } if (_selectionEntries == null || _selectionEntries.Count == 0) { EditorGUILayout.HelpBox("在 Project 窗口选中资产后点击「读取选中项」。", MessageType.Info); return; } EditorGUILayout.Space(4); using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) { EditorGUILayout.LabelField("资产路径", _boldStyle, GUILayout.ExpandWidth(true)); EditorGUILayout.LabelField("注册地址", _boldStyle, GUILayout.Width(200)); EditorGUILayout.LabelField("状态", _boldStyle, GUILayout.Width(80)); } _selectionScrollPos = EditorGUILayout.BeginScrollView(_selectionScrollPos, GUILayout.ExpandHeight(true)); foreach (var entry in _selectionEntries) { using (new EditorGUILayout.HorizontalScope(GUILayout.Height(18))) { EditorGUILayout.LabelField(entry.AssetPath, GUILayout.ExpandWidth(true)); entry.Address = EditorGUILayout.TextField(entry.Address, GUILayout.Width(200)); var label = entry.AlreadyRegistered ? "✅ 已有" : "待注册"; var style = entry.AlreadyRegistered ? _okStyle : EditorStyles.miniLabel; EditorGUILayout.LabelField(label, style, GUILayout.Width(80)); } } EditorGUILayout.EndScrollView(); int newCount = _selectionEntries.Count(e => !e.AlreadyRegistered); EditorGUILayout.LabelField($"共 {_selectionEntries.Count} 项,{newCount} 待注册", EditorStyles.miniLabel); } // ══ 共用选项区 ════════════════════════════════════════════════════════ private void DrawSharedOptions() { EditorGUILayout.LabelField("── 注册选项 ──", EditorStyles.boldLabel); using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.LabelField("目标分组", GUILayout.Width(70)); if (_groupNames != null && _groupNames.Length > 0) { _targetGroupIndex = EditorGUILayout.Popup(_targetGroupIndex, _groupNames, GUILayout.Width(200)); } EditorGUILayout.Space(8); EditorGUILayout.LabelField("标签", GUILayout.Width(30)); _newLabel = EditorGUILayout.TextField(_newLabel, GUILayout.Width(120)); GUILayout.Label("(留空则不添加标签)", EditorStyles.miniLabel); GUILayout.FlexibleSpace(); } using (new EditorGUILayout.HorizontalScope()) { _overwriteAddress = GUILayout.Toggle(_overwriteAddress, "已注册的资产也覆盖地址"); GUILayout.FlexibleSpace(); if (GUILayout.Button("新建分组…", GUILayout.Width(100))) ShowCreateGroupDialog(); } } // ══ 逻辑:Tab ① ══════════════════════════════════════════════════════ private void RefreshKeyEntries() { _keyEntries = new List(); var settings = AddressableAssetSettingsDefaultObject.Settings; if (settings == null) return; // 收集所有已注册地址 → 地址字符串 → 资产路径 var registeredMap = new Dictionary(StringComparer.Ordinal); foreach (var group in settings.groups) { if (group == null) continue; foreach (var e in group.entries) if (e != null) registeredMap[e.address] = e.AssetPath; } // 遍历 AddressKeys 常量 var fields = typeof(AddressKeys) .GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) .Where(f => f.IsLiteral && !f.IsInitOnly && f.FieldType == typeof(string)); foreach (var field in fields) { var key = (string)field.GetRawConstantValue(); var entry = new KeySyncEntry { FieldName = field.Name, AddressKey = key }; if (registeredMap.TryGetValue(key, out string existingPath)) { entry.IsRegistered = true; entry.ExistingAssetPath = existingPath; } else if (_autoSearch) { // 从地址 key 派生搜索名:取最后一段,去掉前缀(ENM_、VFX_ 等) string searchName = DeriveName(key); string[] guids = AssetDatabase.FindAssets(searchName); if (guids.Length > 0) { // 优先取名称完全匹配的;排除文件夹、脚本及程序集定义文件 string best = guids .Select(AssetDatabase.GUIDToAssetPath) .Where(p => !AssetDatabase.IsValidFolder(p) && IsAddressableAssetPath(p)) .OrderBy(p => ExactNameMatch(p, searchName) ? 0 : 1) .FirstOrDefault(); entry.FoundAssetPath = best; entry.FoundGuid = best != null ? AssetDatabase.AssetPathToGUID(best) : null; } } _keyEntries.Add(entry); } } private void RegisterKeyEntry(KeySyncEntry entry) { if (entry.FoundAssetPath == null) return; string groupOverride = _autoGroupByPrefix ? DeriveGroupName(entry.AddressKey) : null; Register(entry.FoundGuid, entry.AddressKey, groupOverride); entry.IsRegistered = true; entry.ExistingAssetPath = entry.FoundAssetPath; entry.FoundAssetPath = null; SaveSettings(); } private void RegisterKeyEntryManual(KeySyncEntry entry) { string path = AssetDatabase.GetAssetPath(entry.ManualAsset); string guid = AssetDatabase.AssetPathToGUID(path); string groupOverride = _autoGroupByPrefix ? DeriveGroupName(entry.AddressKey) : null; Register(guid, entry.AddressKey, groupOverride); entry.IsRegistered = true; entry.ExistingAssetPath = path; entry.ManualAsset = null; SaveSettings(); } private void RegisterAllMatchedKeys() { int count = 0; foreach (var entry in _keyEntries.Where(e => !e.IsRegistered && e.FoundAssetPath != null)) { string groupOverride = _autoGroupByPrefix ? DeriveGroupName(entry.AddressKey) : null; Register(entry.FoundGuid, entry.AddressKey, groupOverride); entry.IsRegistered = true; entry.ExistingAssetPath = entry.FoundAssetPath; entry.FoundAssetPath = null; count++; } Debug.Log($"[AddressableBatch] 已注册 {count} 个 AddressKeys 条目。"); SaveSettings(); } // ══ 逻辑:Tab ② ══════════════════════════════════════════════════════ private void ScanFolder() { _folderEntries = new List(); if (!AssetDatabase.IsValidFolder(_folderPath)) return; var settings = AddressableAssetSettingsDefaultObject.Settings; var registeredGuids = CollectRegisteredGuids(settings); var filters = BuildSearchFilter(); var option = _includeSubfolders ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; string absFolder = Path.GetFullPath(_folderPath); foreach (string filter in filters) { foreach (string absPath in Directory.GetFiles(absFolder, filter, option)) { string relPath = "Assets" + absPath.Substring(Application.dataPath.Length).Replace('\\', '/'); string guid = AssetDatabase.AssetPathToGUID(relPath); if (string.IsNullOrEmpty(guid)) continue; _folderEntries.Add(new FolderEntry { AssetPath = relPath, Guid = guid, Address = BuildAddress(relPath), AlreadyRegistered = registeredGuids.Contains(guid), }); } } // 去重(多个 filter 可能匹配同一文件) _folderEntries = _folderEntries .GroupBy(e => e.Guid) .Select(g => g.First()) .ToList(); } private void RegisterAllFolderEntries() { int count = 0; foreach (var entry in _folderEntries) { if (entry.AlreadyRegistered && !_overwriteAddress) continue; Register(entry.Guid, entry.Address); entry.AlreadyRegistered = true; count++; } Debug.Log($"[AddressableBatch] 文件夹批量注册完成,共注册 {count} 个资产。"); SaveSettings(); } // ══ 逻辑:Tab ③ ══════════════════════════════════════════════════════ private void LoadSelection() { _selectionEntries = new List(); var settings = AddressableAssetSettingsDefaultObject.Settings; var registeredGuids = CollectRegisteredGuids(settings); foreach (string guid in Selection.assetGUIDs) { string path = AssetDatabase.GUIDToAssetPath(guid); if (AssetDatabase.IsValidFolder(path)) { // 展开文件夹 foreach (string sub in AssetDatabase.FindAssets("", new[] { path })) { string subPath = AssetDatabase.GUIDToAssetPath(sub); if (!AssetDatabase.IsValidFolder(subPath)) AddSelectionEntry(sub, subPath, registeredGuids); } } else { AddSelectionEntry(guid, path, registeredGuids); } } _selectionEntries = _selectionEntries .GroupBy(e => e.Guid) .Select(g => g.First()) .ToList(); } private void AddSelectionEntry(string guid, string path, HashSet registeredGuids) { _selectionEntries.Add(new SelectionEntry { AssetPath = path, Guid = guid, Address = BuildAddress(path), AlreadyRegistered = registeredGuids.Contains(guid), }); } private void RegisterAllSelectionEntries() { int count = 0; foreach (var entry in _selectionEntries) { if (entry.AlreadyRegistered && !_overwriteAddress) continue; Register(entry.Guid, entry.Address); entry.AlreadyRegistered = true; count++; } Debug.Log($"[AddressableBatch] 选中资产注册完成,共注册 {count} 个资产。"); SaveSettings(); } // ══ 核心注册 API ═════════════════════════════════════════════════════ private void Register(string guid, string address, string groupNameOverride = null) { if (string.IsNullOrEmpty(guid)) return; var settings = AddressableAssetSettingsDefaultObject.Settings; var group = groupNameOverride != null ? GetOrCreateGroup(settings, groupNameOverride) : GetTargetGroup(settings); if (group == null) return; AddressableAssetEntry entry = settings.FindAssetEntry(guid) ?? settings.CreateOrMoveEntry(guid, group, false, false); if (entry == null) return; if (_overwriteAddress || entry.address != address) entry.address = address; settings.MoveEntry(entry, group, false, false); if (!string.IsNullOrWhiteSpace(_newLabel)) entry.SetLabel(_newLabel.Trim(), true, true); } private AddressableAssetGroup GetOrCreateGroup(AddressableAssetSettings settings, string groupName) { var existing = settings.groups.FirstOrDefault(g => g != null && g.name == groupName); if (existing != null) return existing; var template = settings.GroupTemplateObjects.FirstOrDefault() as AddressableAssetGroupTemplate; var newGroup = settings.CreateGroup(groupName, false, false, true, template != null ? new List(template.SchemaObjects) : null); if (newGroup != null) { RefreshGroupNames(); Debug.Log($"[AddressableBatch] 已自动创建分组:{groupName}"); } return newGroup ?? settings.DefaultGroup; } private AddressableAssetGroup GetTargetGroup(AddressableAssetSettings settings) { RefreshGroupNames(); if (_groupNames == null || _groupNames.Length == 0) return settings.DefaultGroup; string name = _groupNames[Mathf.Clamp(_targetGroupIndex, 0, _groupNames.Length - 1)]; return settings.groups.FirstOrDefault(g => g != null && g.name == name) ?? settings.DefaultGroup; } private static void SaveSettings() { AssetDatabase.SaveAssets(); AddressableAssetSettingsDefaultObject.Settings?.SetDirty( AddressableAssetSettings.ModificationEvent.EntryModified, null, true); } // ══ 创建分组 ══════════════════════════════════════════════════════════ private void ShowCreateGroupDialog() { _newGroupName = EditorInputDialog.Show("新建 Addressable 分组", "请输入分组名称:", _newGroupName); if (string.IsNullOrWhiteSpace(_newGroupName)) return; var settings = AddressableAssetSettingsDefaultObject.Settings; var template = settings.GroupTemplateObjects.FirstOrDefault() as AddressableAssetGroupTemplate; var newGroup = settings.CreateGroup(_newGroupName.Trim(), false, false, true, template != null ? new List(template.SchemaObjects) : null); if (newGroup != null) { Debug.Log($"[AddressableBatch] 已创建分组:{newGroup.name}"); RefreshGroupNames(); _targetGroupIndex = Array.IndexOf(_groupNames, newGroup.name); } } // ══ 辅助 ══════════════════════════════════════════════════════════════ private void RefreshGroupNames() { var settings = AddressableAssetSettingsDefaultObject.Settings; if (settings == null) { _groupNames = Array.Empty(); return; } _groupNames = settings.groups .Where(g => g != null) .Select(g => g.name) .ToArray(); _targetGroupIndex = Mathf.Clamp(_targetGroupIndex, 0, Mathf.Max(0, _groupNames.Length - 1)); } private static HashSet CollectRegisteredGuids(AddressableAssetSettings settings) { var set = new HashSet(StringComparer.Ordinal); if (settings == null) return set; foreach (var group in settings.groups) { if (group == null) continue; foreach (var e in group.entries) if (e != null) set.Add(e.guid); } return set; } private string BuildAddress(string assetPath) { string fileName = Path.GetFileNameWithoutExtension(assetPath); return _addressFormat switch { AddressFormat.FileName => fileName, AddressFormat.FullAssetPath => assetPath, AddressFormat.RelativeToFolder => MakeRelativePath(assetPath, _folderPath), AddressFormat.PrefixPlusFileName => _addressPrefix + fileName, AddressFormat.PrefixPlusRelativePath=> _addressPrefix + MakeRelativePath(assetPath, _folderPath), _ => fileName, }; } private static string MakeRelativePath(string assetPath, string baseFolderPath) { if (string.IsNullOrEmpty(baseFolderPath)) return assetPath; return assetPath.StartsWith(baseFolderPath) ? assetPath.Substring(baseFolderPath.Length).TrimStart('/') : assetPath; } private List BuildSearchFilter() { var list = new List(); if (_filterPrefab) list.Add("*.prefab"); if (_filterScene) list.Add("*.unity"); if (_filterSO) list.Add("*.asset"); if (_filterTexture) { list.Add("*.png"); list.Add("*.jpg"); list.Add("*.tga"); } if (_filterAudio) { list.Add("*.mp3"); list.Add("*.wav"); list.Add("*.ogg"); } if (list.Count == 0) list.Add("*.*"); return list; } /// 从 AddressKey(如 "ENM_GruntWarrior")派生搜索名("GruntWarrior")。 private static string DeriveName(string key) { // 取最后一个 '/' 之后的部分(Config/FootstepCatalog → FootstepCatalog) int slash = key.LastIndexOf('/'); string last = slash >= 0 ? key.Substring(slash + 1) : key; // 去掉前缀(ENM_, VFX_, PROJ_ 等):找第一个 '_' 并截断前缀 int underscore = last.IndexOf('_'); return underscore >= 0 && underscore < last.Length - 1 ? last.Substring(underscore + 1) : last; } /// 根据 AddressKey 前缀返回建议分组名,未匹配时返回 null(回退到手动选定分组)。 private static string DeriveGroupName(string key) => AddressableRules.GetExpectedGroup(key); private static bool ExactNameMatch(string assetPath, string searchName) { string name = Path.GetFileNameWithoutExtension(assetPath); return string.Equals(name, searchName, StringComparison.OrdinalIgnoreCase); } /// /// 判断路径是否为可寻址资产(排除脚本、程序集定义、Shader 等代码文件)。 /// private static bool IsAddressableAssetPath(string path) { string ext = Path.GetExtension(path); if (string.IsNullOrEmpty(ext)) return false; // 排除代码 / 元数据类文件 return ext != ".cs" && ext != ".asmdef" && ext != ".asmref" && ext != ".shader" && ext != ".hlsl" && ext != ".cginc" && ext != ".glsl" && ext != ".json" && ext != ".xml" && ext != ".txt" && ext != ".md"; } private void InitStyles() { if (_stylesInitialized) return; _headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 14 }; _okStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = new Color(0.2f, 0.8f, 0.2f) } }; _warnStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = new Color(1f, 0.6f, 0.1f) } }; _boldStyle = new GUIStyle(EditorStyles.miniLabel) { fontStyle = FontStyle.Bold }; _stylesInitialized = true; } // ══ 数据结构 ══════════════════════════════════════════════════════════ private class KeySyncEntry { public string FieldName; public string AddressKey; public bool IsRegistered; public string ExistingAssetPath; public string FoundAssetPath; public string FoundGuid; public UnityEngine.Object ManualAsset; } private class FolderEntry { public string AssetPath; public string Guid; public string Address; public bool AlreadyRegistered; } private class SelectionEntry { public string AssetPath; public string Guid; public string Address; public bool AlreadyRegistered; } private enum AddressFormat { [InspectorName("文件名(推荐)")] FileName, [InspectorName("完整 Asset 路径")] FullAssetPath, [InspectorName("相对于选定文件夹")] RelativeToFolder, [InspectorName("前缀 + 文件名")] PrefixPlusFileName, [InspectorName("前缀 + 相对路径")] PrefixPlusRelativePath, } } // ── 轻量输入对话框(避免依赖 EditorInputDialog 插件)───────────────────── internal static class EditorInputDialog { /// 弹出单行文本输入对话框。返回用户输入,取消则返回原始默认值。 public static string Show(string title, string message, string defaultValue = "") { string result = defaultValue; // 通过简单的 EditorWindow 实现 var win = ScriptableObject.CreateInstance(); win.Init(title, message, defaultValue, v => { result = v; }); win.ShowModal(); return result; } } internal class InputDialogWindow : EditorWindow { private string _title; private string _message; private string _value; private Action _onConfirm; public void Init(string title, string message, string defaultValue, Action onConfirm) { titleContent = new GUIContent(title); _title = title; _message = message; _value = defaultValue; _onConfirm = onConfirm; minSize = maxSize = new Vector2(340, 110); } private void OnGUI() { EditorGUILayout.Space(8); EditorGUILayout.LabelField(_message); GUI.SetNextControlName("input"); _value = EditorGUILayout.TextField(_value); EditorGUI.FocusTextInControl("input"); EditorGUILayout.Space(8); using (new EditorGUILayout.HorizontalScope()) { GUILayout.FlexibleSpace(); if (GUILayout.Button("取消", GUILayout.Width(70))) Close(); if (GUILayout.Button("确认", GUILayout.Width(70))) { _onConfirm?.Invoke(_value); Close(); } } } } }