File directory changes (mirror Scripts/ module structure): - AbilityTypeDrawer.cs → Equipment/ - CharacterWizardWindow.cs → Character/ - FormEditorWindow.cs → Player/ - GMToolWindow.cs → Tools/ - SOManagerWindow.cs → Tools/ - Map/MapRoomDataEditor.cs → World/Map/ - Navigation/ (root) → Enemies/Navigation/ - Achievements/ → Progression/ Menu hierarchy changes (BaseGames/ top-level): - Data/: +Character Wizard (from Tools/), +Boss Skill Sequence (from Tools/) - Addressables/: +Addressable Batch Tool, +Asset Reference Graph, +Validate Address Keys (from Tools/Verification/) - Scene/Setup/: +Boot Flow Wizard, +Scaffold *, +Auto-Open Persistent (from Tools/) - Scene/: +Camera Area Setup (from Camera/), +Bake All NavSurfaces (from Tools/) - Events/: +Event Bus Monitor, +Event Chain Viewer, +Create/Reimport Event Channels (from Tools/) - Tools/Validation/: +Validate All SOs, +Apply/Validate Script Order (from Tools/ flat) - Tools/Maintenance/: +Missing Scripts/*, +Physics2D Layer Matrix/* (from Tools/ flat) Result: BaseGames/Tools/ reduced from 16 flat items to 4 items + 2 submenus Docs: update AssetFolderSpec §12 editor tool table with new menu paths Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
870 lines
39 KiB
C#
870 lines
39 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// Addressable 批量注册工具
|
||
/// 菜单:BaseGames → Tools → Addressable Batch Tool (Alt+Shift+A)
|
||
///
|
||
/// 三种工作流:
|
||
/// ① 同步 AddressKeys — 读取 AddressKeys.cs 中的常量,按名称在 Project 中搜索对应资产并自动注册
|
||
/// ② 文件夹批量注册 — 拖入文件夹,将其下所有指定类型的资产注册到选定分组,地址格式可配置
|
||
/// ③ 选中资产注册 — 在 Project 窗口选中资产后,一键批量注册到选定分组
|
||
/// </summary>
|
||
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<KeySyncEntry> _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<FolderEntry> _folderEntries;
|
||
private Vector2 _folderScrollPos;
|
||
|
||
// Tab ③
|
||
private List<SelectionEntry> _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<AddressableBatchTool>(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<KeySyncEntry>();
|
||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||
if (settings == null) return;
|
||
|
||
// 收集所有已注册地址 → 地址字符串 → 资产路径
|
||
var registeredMap = new Dictionary<string, string>(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<FolderEntry>();
|
||
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<SelectionEntry>();
|
||
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<string> 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<AddressableAssetGroupSchema>(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<AddressableAssetGroupSchema>(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<string>(); 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<string> CollectRegisteredGuids(AddressableAssetSettings settings)
|
||
{
|
||
var set = new HashSet<string>(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<string> BuildSearchFilter()
|
||
{
|
||
var list = new List<string>();
|
||
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;
|
||
}
|
||
|
||
/// <summary>从 AddressKey(如 "ENM_GruntWarrior")派生搜索名("GruntWarrior")。</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>根据 AddressKey 前缀返回建议分组名,未匹配时返回 null(回退到手动选定分组)。</summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断路径是否为可寻址资产(排除脚本、程序集定义、Shader 等代码文件)。
|
||
/// </summary>
|
||
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
|
||
{
|
||
/// <summary>弹出单行文本输入对话框。返回用户输入,取消则返回原始默认值。</summary>
|
||
public static string Show(string title, string message, string defaultValue = "")
|
||
{
|
||
string result = defaultValue;
|
||
|
||
// 通过简单的 EditorWindow 实现
|
||
var win = ScriptableObject.CreateInstance<InputDialogWindow>();
|
||
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<string> _onConfirm;
|
||
|
||
public void Init(string title, string message, string defaultValue, Action<string> 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();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|