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/Tools/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 ① 自动分组用)
private static readonly (string Prefix, string GroupName)[] PrefixGroupMap =
{
("Scene_", "Scenes"),
("PLY_", "Player"),
("ENM_", "Enemies"),
("PROJ_", "Projectiles"),
("VFX_", "VFX"),
("UI_", "UI"),
("COL_", "Collectibles"),
("WPN_", "Weapons"),
("Config/", "Config"),
};
// 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/Verification/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)
{
foreach (var (prefix, groupName) in PrefixGroupMap)
if (key.StartsWith(prefix, StringComparison.Ordinal))
return groupName;
return null;
}
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();
}
}
}
}
}