feat: Enhance Addressable tools with improved scanning and filtering features

- Updated AddressReferenceGraphWindow to scan for AddressKeys in the _Game directory and added a warning for missing directories.
- Enhanced AddressableBatchTool with new filters for asset types (Prefab, Scene, ScriptableObject, Texture, Audio) and improved UI layout for better usability.
- Introduced automatic application of grouping and labeling rules during registration in AddressableBatchTool.
- Added functionality to quickly scan the _Game folder and improved address building logic.
- Updated AddressableRuleSyncWindow to include handling for custom labels and improved reporting of issues.
- Enhanced AddressableRules with a whitelist for known labels and refined grouping and labeling logic based on asset prefixes.
This commit is contained in:
2026-05-22 13:34:47 +08:00
parent e7b44e1d60
commit f1c0b65737
4 changed files with 416 additions and 112 deletions

View File

@@ -200,11 +200,15 @@ namespace BaseGames.Editor
foreach (var kv in keyDict) foreach (var kv in keyDict)
kv.Value.ExistsInAddressables = registeredAddressValues.Contains(kv.Value.Value); kv.Value.ExistsInAddressables = registeredAddressValues.Contains(kv.Value.Value);
// 3. 扫描 .cs 文件引用 // 3. 扫描 _Game/ 下所有 .cs 文件中对 AddressKeys 的引用
var csFiles = Directory.GetFiles( string gameDir = Path.Combine(Application.dataPath, "_Game");
Path.Combine(Application.dataPath, "Scripts"), if (!Directory.Exists(gameDir))
"*.cs", {
SearchOption.AllDirectories); Debug.LogWarning($"[AddressReferenceGraph] 未找到游戏目录:{gameDir}");
foreach (var kv in keyDict) _entries.Add(kv.Value);
return;
}
var csFiles = Directory.GetFiles(gameDir, "*.cs", SearchOption.AllDirectories);
foreach (var file in csFiles) foreach (var file in csFiles)
{ {

View File

@@ -60,6 +60,11 @@ namespace BaseGames.Editor
// Tab ③ // Tab ③
private List<SelectionEntry> _selectionEntries; private List<SelectionEntry> _selectionEntries;
private Vector2 _selectionScrollPos; private Vector2 _selectionScrollPos;
private bool _selFilterPrefab = true;
private bool _selFilterScene = true;
private bool _selFilterSO = true;
private bool _selFilterTexture;
private bool _selFilterAudio;
// 共用 // 共用
private int _targetGroupIndex; private int _targetGroupIndex;
@@ -67,6 +72,7 @@ namespace BaseGames.Editor
private string _newGroupName = "New Group"; private string _newGroupName = "New Group";
private string _newLabel = ""; private string _newLabel = "";
private bool _overwriteAddress; private bool _overwriteAddress;
private bool _applyRulesOnRegister = true;
// ── 样式(惰性初始化)──────────────────────────────────────────────── // ── 样式(惰性初始化)────────────────────────────────────────────────
private GUIStyle _headerStyle; private GUIStyle _headerStyle;
@@ -81,7 +87,7 @@ namespace BaseGames.Editor
public static void OpenWindow() public static void OpenWindow()
{ {
var win = GetWindow<AddressableBatchTool>(Title); var win = GetWindow<AddressableBatchTool>(Title);
win.minSize = new Vector2(600, 460); win.minSize = new Vector2(920, 520);
win.Show(); win.Show();
} }
@@ -144,9 +150,11 @@ namespace BaseGames.Editor
// 列表表头 // 列表表头
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
{ {
EditorGUILayout.LabelField("常量名", _boldStyle, GUILayout.Width(200)); EditorGUILayout.LabelField("常量名", _boldStyle, GUILayout.Width(180));
EditorGUILayout.LabelField("地址 Key", _boldStyle, GUILayout.Width(180)); EditorGUILayout.LabelField("地址 Key", _boldStyle, GUILayout.Width(160));
EditorGUILayout.LabelField("状态", _boldStyle, GUILayout.Width(100)); EditorGUILayout.LabelField("期望分组", _boldStyle, GUILayout.Width(110));
EditorGUILayout.LabelField("期望标签", _boldStyle, GUILayout.Width(140));
EditorGUILayout.LabelField("状态", _boldStyle, GUILayout.Width(80));
EditorGUILayout.LabelField("匹配资产", _boldStyle); EditorGUILayout.LabelField("匹配资产", _boldStyle);
} }
@@ -160,24 +168,30 @@ namespace BaseGames.Editor
{ {
using (new EditorGUILayout.HorizontalScope(GUILayout.Height(20))) using (new EditorGUILayout.HorizontalScope(GUILayout.Height(20)))
{ {
EditorGUILayout.LabelField(entry.FieldName, GUILayout.Width(200)); EditorGUILayout.LabelField(entry.FieldName, GUILayout.Width(180));
EditorGUILayout.LabelField(entry.AddressKey, GUILayout.Width(180)); EditorGUILayout.LabelField(entry.AddressKey, GUILayout.Width(160));
EditorGUILayout.LabelField(
AddressableRules.GetExpectedGroup(entry.AddressKey) ?? "Default",
GUILayout.Width(110));
EditorGUILayout.LabelField(
FormatLabels(AddressableRules.GetExpectedLabels(entry.AddressKey)),
GUILayout.Width(140));
if (entry.IsRegistered) if (entry.IsRegistered)
{ {
EditorGUILayout.LabelField("✅ 已注册", _okStyle, GUILayout.Width(100)); EditorGUILayout.LabelField("✅ 已注册", _okStyle, GUILayout.Width(80));
EditorGUILayout.LabelField(entry.ExistingAssetPath ?? "—"); EditorGUILayout.LabelField(entry.ExistingAssetPath ?? "—");
} }
else if (entry.FoundAssetPath != null) else if (entry.FoundAssetPath != null)
{ {
EditorGUILayout.LabelField("⚠ 未注册", _warnStyle, GUILayout.Width(100)); EditorGUILayout.LabelField("⚠ 未注册", _warnStyle, GUILayout.Width(80));
EditorGUILayout.LabelField(entry.FoundAssetPath, GUILayout.ExpandWidth(true)); EditorGUILayout.LabelField(entry.FoundAssetPath, GUILayout.ExpandWidth(true));
if (GUILayout.Button("注册", GUILayout.Width(50))) if (GUILayout.Button("注册", GUILayout.Width(50)))
RegisterKeyEntry(entry); RegisterKeyEntry(entry);
} }
else else
{ {
EditorGUILayout.LabelField("❌ 未找到", _warnStyle, GUILayout.Width(100)); EditorGUILayout.LabelField("❌ 未找到", _warnStyle, GUILayout.Width(80));
entry.ManualAsset = (UnityEngine.Object)EditorGUILayout.ObjectField( entry.ManualAsset = (UnityEngine.Object)EditorGUILayout.ObjectField(
entry.ManualAsset, typeof(UnityEngine.Object), false); entry.ManualAsset, typeof(UnityEngine.Object), false);
if (entry.ManualAsset != null) if (entry.ManualAsset != null)
@@ -247,6 +261,8 @@ namespace BaseGames.Editor
using (new EditorGUILayout.HorizontalScope()) using (new EditorGUILayout.HorizontalScope())
{ {
if (GUILayout.Button("⚡ 全量扫描 _Game/", GUILayout.Width(150)))
QuickScanGameFolder();
GUILayout.FlexibleSpace(); GUILayout.FlexibleSpace();
GUI.enabled = !string.IsNullOrEmpty(_folderPath); GUI.enabled = !string.IsNullOrEmpty(_folderPath);
if (GUILayout.Button("扫描文件夹", GUILayout.Width(100))) if (GUILayout.Button("扫描文件夹", GUILayout.Width(100)))
@@ -266,9 +282,14 @@ namespace BaseGames.Editor
EditorGUILayout.Space(4); EditorGUILayout.Space(4);
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
{ {
EditorGUILayout.LabelField("资产路径", _boldStyle, GUILayout.ExpandWidth(true)); EditorGUILayout.LabelField("资产路径", _boldStyle, GUILayout.Width(180));
EditorGUILayout.LabelField("预计地址", _boldStyle, GUILayout.Width(200)); EditorGUILayout.LabelField("地址", _boldStyle, GUILayout.Width(150));
EditorGUILayout.LabelField("状态", _boldStyle, GUILayout.Width(80)); if (_applyRulesOnRegister)
{
EditorGUILayout.LabelField("分组(规则)", _boldStyle, GUILayout.Width(120));
EditorGUILayout.LabelField("标签(规则)", _boldStyle, GUILayout.Width(150));
}
EditorGUILayout.LabelField("状态", _boldStyle, GUILayout.Width(70));
} }
_folderScrollPos = EditorGUILayout.BeginScrollView(_folderScrollPos, GUILayout.ExpandHeight(true)); _folderScrollPos = EditorGUILayout.BeginScrollView(_folderScrollPos, GUILayout.ExpandHeight(true));
@@ -276,11 +297,16 @@ namespace BaseGames.Editor
{ {
using (new EditorGUILayout.HorizontalScope(GUILayout.Height(18))) using (new EditorGUILayout.HorizontalScope(GUILayout.Height(18)))
{ {
EditorGUILayout.LabelField(entry.AssetPath, GUILayout.ExpandWidth(true)); EditorGUILayout.LabelField(entry.AssetPath, GUILayout.Width(180));
entry.Address = EditorGUILayout.TextField(entry.Address, GUILayout.Width(200)); entry.Address = EditorGUILayout.TextField(entry.Address, GUILayout.Width(150));
if (_applyRulesOnRegister)
{
EditorGUILayout.LabelField(entry.PredictedGroup ?? "Default", GUILayout.Width(120));
EditorGUILayout.LabelField(entry.PredictedLabels ?? "—", GUILayout.Width(150));
}
var label = entry.AlreadyRegistered ? "✅ 已有" : "待注册"; var label = entry.AlreadyRegistered ? "✅ 已有" : "待注册";
var style = entry.AlreadyRegistered ? _okStyle : EditorStyles.miniLabel; var style = entry.AlreadyRegistered ? _okStyle : EditorStyles.miniLabel;
EditorGUILayout.LabelField(label, style, GUILayout.Width(80)); EditorGUILayout.LabelField(label, style, GUILayout.Width(70));
} }
} }
EditorGUILayout.EndScrollView(); EditorGUILayout.EndScrollView();
@@ -296,6 +322,18 @@ namespace BaseGames.Editor
EditorGUILayout.LabelField("在 Project 窗口中选中资产或文件夹,然后点击「读取选中项」。", EditorStyles.wordWrappedMiniLabel); EditorGUILayout.LabelField("在 Project 窗口中选中资产或文件夹,然后点击「读取选中项」。", EditorStyles.wordWrappedMiniLabel);
EditorGUILayout.Space(4); EditorGUILayout.Space(4);
// 资产类型筛选(与 Tab ② 一致,防止误注册不该 Addressable 的文件类型)
EditorGUILayout.LabelField("资产类型筛选", EditorStyles.boldLabel);
using (new EditorGUILayout.HorizontalScope())
{
_selFilterPrefab = GUILayout.Toggle(_selFilterPrefab, "Prefab", GUILayout.Width(70));
_selFilterScene = GUILayout.Toggle(_selFilterScene, "Scene", GUILayout.Width(70));
_selFilterSO = GUILayout.Toggle(_selFilterSO, "SO/Asset", GUILayout.Width(80));
_selFilterTexture = GUILayout.Toggle(_selFilterTexture, "Texture", GUILayout.Width(70));
_selFilterAudio = GUILayout.Toggle(_selFilterAudio, "Audio", GUILayout.Width(70));
}
EditorGUILayout.Space(4);
using (new EditorGUILayout.HorizontalScope()) using (new EditorGUILayout.HorizontalScope())
{ {
GUILayout.FlexibleSpace(); GUILayout.FlexibleSpace();
@@ -316,9 +354,14 @@ namespace BaseGames.Editor
EditorGUILayout.Space(4); EditorGUILayout.Space(4);
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
{ {
EditorGUILayout.LabelField("资产路径", _boldStyle, GUILayout.ExpandWidth(true)); EditorGUILayout.LabelField("资产路径", _boldStyle, GUILayout.Width(180));
EditorGUILayout.LabelField("注册地址", _boldStyle, GUILayout.Width(200)); EditorGUILayout.LabelField("注册地址", _boldStyle, GUILayout.Width(150));
EditorGUILayout.LabelField("状态", _boldStyle, GUILayout.Width(80)); if (_applyRulesOnRegister)
{
EditorGUILayout.LabelField("分组(规则)", _boldStyle, GUILayout.Width(120));
EditorGUILayout.LabelField("标签(规则)", _boldStyle, GUILayout.Width(150));
}
EditorGUILayout.LabelField("状态", _boldStyle, GUILayout.Width(70));
} }
_selectionScrollPos = EditorGUILayout.BeginScrollView(_selectionScrollPos, GUILayout.ExpandHeight(true)); _selectionScrollPos = EditorGUILayout.BeginScrollView(_selectionScrollPos, GUILayout.ExpandHeight(true));
@@ -326,11 +369,16 @@ namespace BaseGames.Editor
{ {
using (new EditorGUILayout.HorizontalScope(GUILayout.Height(18))) using (new EditorGUILayout.HorizontalScope(GUILayout.Height(18)))
{ {
EditorGUILayout.LabelField(entry.AssetPath, GUILayout.ExpandWidth(true)); EditorGUILayout.LabelField(entry.AssetPath, GUILayout.Width(180));
entry.Address = EditorGUILayout.TextField(entry.Address, GUILayout.Width(200)); entry.Address = EditorGUILayout.TextField(entry.Address, GUILayout.Width(150));
if (_applyRulesOnRegister)
{
EditorGUILayout.LabelField(entry.PredictedGroup ?? "Default", GUILayout.Width(120));
EditorGUILayout.LabelField(entry.PredictedLabels ?? "—", GUILayout.Width(150));
}
var label = entry.AlreadyRegistered ? "✅ 已有" : "待注册"; var label = entry.AlreadyRegistered ? "✅ 已有" : "待注册";
var style = entry.AlreadyRegistered ? _okStyle : EditorStyles.miniLabel; var style = entry.AlreadyRegistered ? _okStyle : EditorStyles.miniLabel;
EditorGUILayout.LabelField(label, style, GUILayout.Width(80)); EditorGUILayout.LabelField(label, style, GUILayout.Width(70));
} }
} }
EditorGUILayout.EndScrollView(); EditorGUILayout.EndScrollView();
@@ -347,22 +395,32 @@ namespace BaseGames.Editor
using (new EditorGUILayout.HorizontalScope()) using (new EditorGUILayout.HorizontalScope())
{ {
EditorGUILayout.LabelField("目标分组", GUILayout.Width(70)); EditorGUILayout.LabelField("目标分组", GUILayout.Width(70));
if (_groupNames != null && _groupNames.Length > 0) // 自动规则模式下,目标分组由规则决定,手动选择无效
GUI.enabled = !_applyRulesOnRegister;
if (_applyRulesOnRegister)
{
EditorGUILayout.LabelField("(由 AddressableRules 自动决定)",
EditorStyles.miniLabel, GUILayout.Width(200));
}
else if (_groupNames != null && _groupNames.Length > 0)
{ {
_targetGroupIndex = EditorGUILayout.Popup(_targetGroupIndex, _targetGroupIndex = EditorGUILayout.Popup(_targetGroupIndex,
_groupNames, GUILayout.Width(200)); _groupNames, GUILayout.Width(200));
} }
GUI.enabled = true;
EditorGUILayout.Space(8); EditorGUILayout.Space(8);
EditorGUILayout.LabelField("标签", GUILayout.Width(30)); EditorGUILayout.LabelField("附加标签", GUILayout.Width(52));
_newLabel = EditorGUILayout.TextField(_newLabel, GUILayout.Width(120)); _newLabel = EditorGUILayout.TextField(_newLabel, GUILayout.Width(120));
GUILayout.Label("留空则不添加标签", EditorStyles.miniLabel); GUILayout.Label("可在规则标签基础上追加", EditorStyles.miniLabel);
GUILayout.FlexibleSpace(); GUILayout.FlexibleSpace();
} }
using (new EditorGUILayout.HorizontalScope()) using (new EditorGUILayout.HorizontalScope())
{ {
_overwriteAddress = GUILayout.Toggle(_overwriteAddress, "已注册的资产也覆盖地址"); _overwriteAddress = GUILayout.Toggle(_overwriteAddress, "已注册的资产也覆盖地址");
GUILayout.Space(16);
_applyRulesOnRegister = GUILayout.Toggle(_applyRulesOnRegister, "自动应用分组/标签规则");
GUILayout.FlexibleSpace(); GUILayout.FlexibleSpace();
if (GUILayout.Button("新建分组…", GUILayout.Width(100))) if (GUILayout.Button("新建分组…", GUILayout.Width(100)))
ShowCreateGroupDialog(); ShowCreateGroupDialog();
@@ -479,23 +537,45 @@ namespace BaseGames.Editor
string absFolder = Path.GetFullPath(_folderPath); string absFolder = Path.GetFullPath(_folderPath);
// 收集所有文件
var allFiles = new List<string>();
foreach (string filter in filters) foreach (string filter in filters)
allFiles.AddRange(Directory.GetFiles(absFolder, filter, option));
try
{ {
foreach (string absPath in Directory.GetFiles(absFolder, filter, option)) for (int i = 0; i < allFiles.Count; i++)
{ {
string absPath = allFiles[i];
if (i % 20 == 0)
EditorUtility.DisplayProgressBar("扫描文件夹",
Path.GetFileName(absPath),
(float)i / allFiles.Count);
string relPath = "Assets" + absPath.Substring(Application.dataPath.Length).Replace('\\', '/'); string relPath = "Assets" + absPath.Substring(Application.dataPath.Length).Replace('\\', '/');
if (!IsAddressableAssetPath(relPath)) continue;
if (ShouldExclude(relPath)) continue;
string guid = AssetDatabase.AssetPathToGUID(relPath); string guid = AssetDatabase.AssetPathToGUID(relPath);
if (string.IsNullOrEmpty(guid)) continue; if (string.IsNullOrEmpty(guid)) continue;
string addr = BuildAddress(relPath);
_folderEntries.Add(new FolderEntry _folderEntries.Add(new FolderEntry
{ {
AssetPath = relPath, AssetPath = relPath,
Guid = guid, Guid = guid,
Address = BuildAddress(relPath), Address = addr,
AlreadyRegistered = registeredGuids.Contains(guid), AlreadyRegistered = registeredGuids.Contains(guid),
PredictedGroup = AddressableRules.GetExpectedGroup(addr),
PredictedLabels = FormatLabels(AddressableRules.GetExpectedLabels(addr)),
}); });
} }
} }
finally
{
EditorUtility.ClearProgressBar();
}
// 去重(多个 filter 可能匹配同一文件) // 去重(多个 filter 可能匹配同一文件)
_folderEntries = _folderEntries _folderEntries = _folderEntries
@@ -504,6 +584,34 @@ namespace BaseGames.Editor
.ToList(); .ToList();
} }
/// <summary>
/// 返回 true 表示该文件应从 Addressable 注册中排除。
/// 规范来源AddressablesLabelSpec §5.2(禁止注册项)。
/// </summary>
private static bool ShouldExclude(string relPath)
{
string lowerPath = relPath.Replace('\\', '/').ToLowerInvariant();
string fileName = Path.GetFileNameWithoutExtension(relPath);
// 测试场景(放在 Scenes/Testings/ 下)
if (lowerPath.Contains("/scenes/testings/")) return true;
// 事件频道 ScriptableObjectEVT_ 前缀)
if (fileName.StartsWith("EVT_", StringComparison.Ordinal)) return true;
// Sprite Atlas — 随依赖它的 Prefab 隐式打包,不单独注册
if (Path.GetExtension(relPath) == ".spriteatlas") return true;
// Material — 随 Prefab 依赖关系打包,不单独注册
if (Path.GetExtension(relPath) == ".mat") return true;
// HitBox / HurtBox 等碰撞盒子 Prefab子 Prefab不独立寻址
if (fileName.StartsWith("HitBox_", StringComparison.OrdinalIgnoreCase)) return true;
if (fileName.StartsWith("HurtBox_", StringComparison.OrdinalIgnoreCase)) return true;
return false;
}
private void RegisterAllFolderEntries() private void RegisterAllFolderEntries()
{ {
int count = 0; int count = 0;
@@ -554,12 +662,29 @@ namespace BaseGames.Editor
private void AddSelectionEntry(string guid, string path, HashSet<string> registeredGuids) private void AddSelectionEntry(string guid, string path, HashSet<string> registeredGuids)
{ {
if (!IsAddressableAssetPath(path)) return;
if (ShouldExclude(path)) return;
// 资产类型筛选(与 Tab ③ 筛选 Toggle 联动)
string ext = Path.GetExtension(path).ToLowerInvariant();
bool isMatch = (_selFilterPrefab && ext == ".prefab")
|| (_selFilterScene && ext == ".unity")
|| (_selFilterSO && ext == ".asset")
|| (_selFilterTexture && (ext == ".png" || ext == ".jpg" || ext == ".tga"))
|| (_selFilterAudio && (ext == ".mp3" || ext == ".wav" || ext == ".ogg"));
// 若没有任何类型 Toggle 被勾选,则接受所有类型(兜底行为,避免全部筛空)
bool anyToggled = _selFilterPrefab || _selFilterScene || _selFilterSO || _selFilterTexture || _selFilterAudio;
if (anyToggled && !isMatch) return;
string addr = BuildAddress(path);
_selectionEntries.Add(new SelectionEntry _selectionEntries.Add(new SelectionEntry
{ {
AssetPath = path, AssetPath = path,
Guid = guid, Guid = guid,
Address = BuildAddress(path), Address = addr,
AlreadyRegistered = registeredGuids.Contains(guid), AlreadyRegistered = registeredGuids.Contains(guid),
PredictedGroup = AddressableRules.GetExpectedGroup(addr),
PredictedLabels = FormatLabels(AddressableRules.GetExpectedLabels(addr)),
}); });
} }
@@ -584,8 +709,24 @@ namespace BaseGames.Editor
if (string.IsNullOrEmpty(guid)) return; if (string.IsNullOrEmpty(guid)) return;
var settings = AddressableAssetSettingsDefaultObject.Settings; var settings = AddressableAssetSettingsDefaultObject.Settings;
var group = groupNameOverride != null
? GetOrCreateGroup(settings, groupNameOverride) // 重复地址检查:同一 address 已注册到不同 GUID 时提示确认
var existingByAddress = FindEntryByAddress(settings, address);
if (existingByAddress != null && existingByAddress.guid != guid)
{
bool proceed = EditorUtility.DisplayDialog(
"⚠ 地址已存在",
$"地址 \"{address}\" 已注册到:\n{existingByAddress.AssetPath}\n\n" +
$"继续会将该地址重新指向当前资产GUID: {guid})。是否继续?",
"继续", "取消");
if (!proceed) return;
}
// Determine target group: explicit override → rules → manual selection
string effectiveGroup = groupNameOverride
?? (_applyRulesOnRegister ? AddressableRules.GetExpectedGroup(address) : null);
var group = effectiveGroup != null
? GetOrCreateGroup(settings, effectiveGroup)
: GetTargetGroup(settings); : GetTargetGroup(settings);
if (group == null) return; if (group == null) return;
@@ -601,6 +742,28 @@ namespace BaseGames.Editor
if (!string.IsNullOrWhiteSpace(_newLabel)) if (!string.IsNullOrWhiteSpace(_newLabel))
entry.SetLabel(_newLabel.Trim(), true, true); entry.SetLabel(_newLabel.Trim(), true, true);
// Apply rules-based labels
if (_applyRulesOnRegister)
{
foreach (var lbl in AddressableRules.GetExpectedLabels(address))
{
EnsureLabelExists(settings, lbl);
entry.SetLabel(lbl, true, true);
}
}
}
private static AddressableAssetEntry FindEntryByAddress(AddressableAssetSettings settings, string address)
{
if (settings == null) return null;
foreach (var group in settings.groups)
{
if (group == null) continue;
foreach (var e in group.entries)
if (e != null && e.address == address) return e;
}
return null;
} }
private AddressableAssetGroup GetOrCreateGroup(AddressableAssetSettings settings, string groupName) private AddressableAssetGroup GetOrCreateGroup(AddressableAssetSettings settings, string groupName)
@@ -636,8 +799,36 @@ namespace BaseGames.Editor
AddressableAssetSettings.ModificationEvent.EntryModified, null, true); AddressableAssetSettings.ModificationEvent.EntryModified, null, true);
} }
private static void EnsureLabelExists(AddressableAssetSettings settings, string label)
{
if (!settings.GetLabels().Contains(label))
{
settings.AddLabel(label, true);
Debug.Log($"[AddressableBatch] 已创建标签:{label}");
}
}
private static string FormatLabels(string[] labels)
=> labels.Length > 0 ? string.Join(", ", labels) : "—";
// ══ 创建分组 ══════════════════════════════════════════════════════════ // ══ 创建分组 ══════════════════════════════════════════════════════════
private void QuickScanGameFolder()
{
_folderPath = "Assets/_Game";
_folderAsset = AssetDatabase.LoadAssetAtPath<DefaultAsset>(_folderPath);
_includeSubfolders = true;
_filterPrefab = true;
_filterScene = true;
_filterSO = true;
_filterAudio = true;
_filterTexture = false;
_addressFormat = AddressFormat.FileName;
_applyRulesOnRegister = true;
_tab = 1;
ScanFolder();
}
private void ShowCreateGroupDialog() private void ShowCreateGroupDialog()
{ {
_newGroupName = EditorInputDialog.Show("新建 Addressable 分组", "请输入分组名称:", _newGroupName); _newGroupName = EditorInputDialog.Show("新建 Addressable 分组", "请输入分组名称:", _newGroupName);
@@ -685,13 +876,27 @@ namespace BaseGames.Editor
private string BuildAddress(string assetPath) private string BuildAddress(string assetPath)
{ {
string fileName = Path.GetFileNameWithoutExtension(assetPath); string fileName = Path.GetFileNameWithoutExtension(assetPath);
string relativePath = MakeRelativePath(assetPath, _folderPath);
return _addressFormat switch return _addressFormat switch
{ {
AddressFormat.FileName => fileName, AddressFormat.FileName => fileName,
AddressFormat.FullAssetPath => assetPath, AddressFormat.FullAssetPath => assetPath,
AddressFormat.RelativeToFolder => MakeRelativePath(assetPath, _folderPath), AddressFormat.RelativeToFolder => relativePath,
AddressFormat.PrefixPlusFileName => _addressPrefix + fileName, // 前缀拼接:前缀以 '/' 结尾时直接连接(如 "Config/" + "FootstepCatalog"
AddressFormat.PrefixPlusRelativePath=> _addressPrefix + MakeRelativePath(assetPath, _folderPath), // 前缀不以 '/' 结尾时用 '_' 连接(如 "Room_Forest" + "_01"
AddressFormat.PrefixPlusFileName =>
string.IsNullOrEmpty(_addressPrefix)
? fileName
: (_addressPrefix.EndsWith("/")
? _addressPrefix + fileName
: _addressPrefix + "_" + fileName),
AddressFormat.PrefixPlusRelativePath =>
string.IsNullOrEmpty(_addressPrefix)
? relativePath
: (_addressPrefix.EndsWith("/")
? _addressPrefix + relativePath
: _addressPrefix + "_" + relativePath),
_ => fileName, _ => fileName,
}; };
} }
@@ -716,6 +921,29 @@ namespace BaseGames.Editor
return list; return list;
} }
/// <summary>
/// 判断路径是否为可寻址资产排除脚本、程序集定义、Shader、Sprite Atlas、Material 等文件)。
/// </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"
&& ext != ".spriteatlas" // 随依赖它的 Prefab 隐式打包
&& ext != ".mat"; // Material 随 Prefab 依赖打包
}
/// <summary>从 AddressKey如 "ENM_GruntWarrior")派生搜索名("GruntWarrior")。</summary> /// <summary>从 AddressKey如 "ENM_GruntWarrior")派生搜索名("GruntWarrior")。</summary>
private static string DeriveName(string key) private static string DeriveName(string key)
{ {
@@ -740,27 +968,6 @@ namespace BaseGames.Editor
return string.Equals(name, searchName, StringComparison.OrdinalIgnoreCase); 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() private void InitStyles()
{ {
if (_stylesInitialized) return; if (_stylesInitialized) return;
@@ -790,6 +997,8 @@ namespace BaseGames.Editor
public string Guid; public string Guid;
public string Address; public string Address;
public bool AlreadyRegistered; public bool AlreadyRegistered;
public string PredictedGroup;
public string PredictedLabels;
} }
private class SelectionEntry private class SelectionEntry
@@ -798,6 +1007,8 @@ namespace BaseGames.Editor
public string Guid; public string Guid;
public string Address; public string Address;
public bool AlreadyRegistered; public bool AlreadyRegistered;
public string PredictedGroup;
public string PredictedLabels;
} }
private enum AddressFormat private enum AddressFormat

View File

@@ -36,11 +36,13 @@ namespace BaseGames.Editor
public string ExpectedGroup; // null = 规则未覆盖,维持现状 public string ExpectedGroup; // null = 规则未覆盖,维持现状
public string[] CurrentLabels; public string[] CurrentLabels;
public string[] ExpectedLabels; public string[] ExpectedLabels;
public string[] MissingLabels; // 应有但没有 public string[] MissingLabels; // 应有但没有(规则要求),红色错误
public string[] ExtraLabels; // 有但不应有 public string[] ExtraLabels; // 规则不要求且在 KnownLabels 中(多余规则标签),红色错误
public string[] UnknownLabels; // 规则不要求且不在 KnownLabels 中(自定义标签),黄色警告,不自动删除
public bool GroupOk => ExpectedGroup == null || CurrentGroup == ExpectedGroup; public bool GroupOk => ExpectedGroup == null || CurrentGroup == ExpectedGroup;
public bool LabelsOk => MissingLabels.Length == 0 && ExtraLabels.Length == 0; public bool LabelsOk => MissingLabels.Length == 0 && ExtraLabels.Length == 0;
public bool IsOk => GroupOk && LabelsOk; public bool IsOk => GroupOk && LabelsOk;
public bool HasWarnings => UnknownLabels.Length > 0;
} }
// ── 状态 ────────────────────────────────────────────────────────────── // ── 状态 ──────────────────────────────────────────────────────────────
@@ -74,7 +76,7 @@ namespace BaseGames.Editor
public static void OpenWindow() public static void OpenWindow()
{ {
var win = GetWindow<AddressableRuleSyncWindow>("Addressable Rule Sync"); var win = GetWindow<AddressableRuleSyncWindow>("Addressable Rule Sync");
win.minSize = new Vector2(900, 520); win.minSize = new Vector2(1040, 540);
win.Show(); win.Show();
} }
@@ -108,6 +110,9 @@ namespace BaseGames.Editor
if (GUILayout.Button("扫描", EditorStyles.toolbarButton, GUILayout.Width(80))) if (GUILayout.Button("扫描", EditorStyles.toolbarButton, GUILayout.Width(80)))
Scan(); Scan();
if (GUILayout.Button("🔄 刷新", EditorStyles.toolbarButton, GUILayout.Width(60)))
Scan();
GUILayout.Space(8); GUILayout.Space(8);
_showOk = GUILayout.Toggle(_showOk, "显示正常项", EditorStyles.toolbarButton, GUILayout.Width(80)); _showOk = GUILayout.Toggle(_showOk, "显示正常项", EditorStyles.toolbarButton, GUILayout.Width(80));
GUILayout.Space(8); GUILayout.Space(8);
@@ -136,10 +141,12 @@ namespace BaseGames.Editor
int total = _reports.Count; int total = _reports.Count;
int ok = _reports.Count(r => r.IsOk); int ok = _reports.Count(r => r.IsOk);
int issues = total - ok; int issues = _reports.Count(r => !r.IsOk);
int warnings = _reports.Count(r => r.IsOk && r.HasWarnings);
int wrongGrp = _reports.Count(r => !r.GroupOk); int wrongGrp = _reports.Count(r => !r.GroupOk);
int misLabel = _reports.Count(r => r.MissingLabels.Length > 0); int misLabel = _reports.Count(r => r.MissingLabels.Length > 0);
int extLabel = _reports.Count(r => r.ExtraLabels.Length > 0); int extLabel = _reports.Count(r => r.ExtraLabels.Length > 0);
int unkLabel = _reports.Count(r => r.UnknownLabels.Length > 0);
EditorGUILayout.Space(2); EditorGUILayout.Space(2);
using (new EditorGUILayout.HorizontalScope()) using (new EditorGUILayout.HorizontalScope())
@@ -148,9 +155,11 @@ namespace BaseGames.Editor
GUILayout.Space(12); GUILayout.Space(12);
DrawColoredLabel($"✅ 正常 {ok}", ColOk); DrawColoredLabel($"✅ 正常 {ok}", ColOk);
GUILayout.Space(12); GUILayout.Space(12);
DrawColoredLabel($" 问题 {issues}", issues > 0 ? ColWarn : ColOk); DrawColoredLabel($" 问题 {issues}", issues > 0 ? ColError : ColOk);
GUILayout.Space(8);
DrawColoredLabel($"⚠ 自定义标签 {unkLabel}", unkLabel > 0 ? ColWarn : ColOk);
GUILayout.Space(20); GUILayout.Space(20);
GUILayout.Label($"分组错误 {wrongGrp} | 标签缺失 {misLabel} | 标签多余 {extLabel}", GUILayout.Label($"分组错误 {wrongGrp} | 标签缺失 {misLabel} | 多余规则标签 {extLabel}",
EditorStyles.miniLabel); EditorStyles.miniLabel);
GUILayout.FlexibleSpace(); GUILayout.FlexibleSpace();
} }
@@ -165,10 +174,11 @@ namespace BaseGames.Editor
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
{ {
GUILayout.Label("Address", _boldStyle, GUILayout.Width(200)); GUILayout.Label("Address", _boldStyle, GUILayout.Width(200));
GUILayout.Label("当前分组", _boldStyle, GUILayout.Width(130)); GUILayout.Label("当前分组", _boldStyle, GUILayout.Width(120));
GUILayout.Label("期望分组", _boldStyle, GUILayout.Width(130)); GUILayout.Label("期望分组", _boldStyle, GUILayout.Width(120));
GUILayout.Label("缺失标签", _boldStyle, GUILayout.Width(140)); GUILayout.Label("缺失标签", _boldStyle, GUILayout.Width(130));
GUILayout.Label("多余标签", _boldStyle, GUILayout.Width(120)); GUILayout.Label("多余规则标签", _boldStyle, GUILayout.Width(110));
GUILayout.Label("自定义标签", _boldStyle, GUILayout.Width(110));
GUILayout.Label("状态", _boldStyle, GUILayout.Width(80)); GUILayout.Label("状态", _boldStyle, GUILayout.Width(80));
} }
@@ -211,29 +221,35 @@ namespace BaseGames.Editor
// 当前分组 // 当前分组
var grpColor = r.GroupOk ? ColOk : ColError; var grpColor = r.GroupOk ? ColOk : ColError;
DrawColoredLabel(r.CurrentGroup ?? "—", grpColor, GUILayout.Width(130)); DrawColoredLabel(r.CurrentGroup ?? "—", grpColor, GUILayout.Width(120));
// 期望分组 // 期望分组
var expGrpText = r.ExpectedGroup ?? "(规则未覆盖)"; var expGrpText = r.ExpectedGroup ?? "(规则未覆盖)";
var expGrpColor = r.GroupOk ? ColOk : ColWarn; var expGrpColor = r.GroupOk ? ColOk : ColWarn;
DrawColoredLabel(expGrpText, expGrpColor, GUILayout.Width(130)); DrawColoredLabel(expGrpText, expGrpColor, GUILayout.Width(120));
// 缺失标签 // 缺失标签(红色,须补齐)
var missingText = r.MissingLabels.Length > 0 ? string.Join(", ", r.MissingLabels) : "—"; var missingText = r.MissingLabels.Length > 0 ? string.Join(", ", r.MissingLabels) : "—";
DrawColoredLabel(missingText, r.MissingLabels.Length > 0 ? ColError : ColOk, GUILayout.Width(140)); DrawColoredLabel(missingText, r.MissingLabels.Length > 0 ? ColError : ColOk, GUILayout.Width(130));
// 多余标签 // 多余规则标签(红色,将被 FixEntry 移除)
var extraText = r.ExtraLabels.Length > 0 ? string.Join(", ", r.ExtraLabels) : "—"; var extraText = r.ExtraLabels.Length > 0 ? string.Join(", ", r.ExtraLabels) : "—";
DrawColoredLabel(extraText, r.ExtraLabels.Length > 0 ? ColWarn : ColOk, GUILayout.Width(120)); DrawColoredLabel(extraText, r.ExtraLabels.Length > 0 ? ColError : ColOk, GUILayout.Width(110));
// 自定义标签(黄色警告,不会被自动删除,建议写入规范)
var unknownText = r.UnknownLabels.Length > 0 ? string.Join(", ", r.UnknownLabels) : "—";
DrawColoredLabel(unknownText, r.UnknownLabels.Length > 0 ? ColWarn : ColOk, GUILayout.Width(110));
// 状态 + 单条修复按钮 // 状态 + 单条修复按钮
if (r.IsOk) if (r.IsOk)
{ {
DrawColoredLabel("✅ 正常", ColOk, GUILayout.Width(80)); var statusColor = r.HasWarnings ? ColWarn : ColOk;
var statusText = r.HasWarnings ? "⚠ 自定义标签" : "✅ 正常";
DrawColoredLabel(statusText, statusColor, GUILayout.Width(80));
} }
else else
{ {
DrawColoredLabel(" 需修复", ColWarn, GUILayout.Width(60)); DrawColoredLabel(" 需修复", ColError, GUILayout.Width(60));
if (GUILayout.Button("修复", EditorStyles.miniButton, GUILayout.Width(40))) if (GUILayout.Button("修复", EditorStyles.miniButton, GUILayout.Width(40)))
FixEntry(r); FixEntry(r);
} }
@@ -247,7 +263,8 @@ namespace BaseGames.Editor
EditorGUILayout.Space(4); EditorGUILayout.Space(4);
EditorGUILayout.HelpBox( EditorGUILayout.HelpBox(
"规则来源Docs/Standards/AddressablesLabelSpec.md §3 分组规则AssetFolderSpec.md §8.1\n" + "规则来源Docs/Standards/AddressablesLabelSpec.md §3 分组规则AssetFolderSpec.md §8.1\n" +
"「修复所有问题」仅修改已注册资产的分组/标签,不注册新资产(请用 Addressable Batch Tool。", "「修复所有问题」仅修改已注册资产的分组/标签,不注册新资产,不删除自定义标签(黄色警告项)。\n" +
"新增资产工作流:① Addressable Batch Tool → ⚡ 全量扫描 _Game/ → 注册所有 ② 返回此窗口 → 扫描 → 修复所有问题",
MessageType.None); MessageType.None);
} }
@@ -259,9 +276,6 @@ namespace BaseGames.Editor
var settings = AddressableAssetSettingsDefaultObject.Settings; var settings = AddressableAssetSettingsDefaultObject.Settings;
if (settings == null) return; if (settings == null) return;
// 收集所有全局标签供"多余标签"判断
var allKnownLabels = new HashSet<string>(settings.GetLabels(), StringComparer.Ordinal);
foreach (var group in settings.groups) foreach (var group in settings.groups)
{ {
if (group == null) continue; if (group == null) continue;
@@ -275,7 +289,13 @@ namespace BaseGames.Editor
var currentLbls = entry.labels.ToArray(); var currentLbls = entry.labels.ToArray();
var missing = expectedLbls.Except(currentLbls, StringComparer.Ordinal).ToArray(); var missing = expectedLbls.Except(currentLbls, StringComparer.Ordinal).ToArray();
var extra = currentLbls.Except(expectedLbls, StringComparer.Ordinal).ToArray();
// 区分两类"多余标签"
// extra = 规则已知标签KnownLabels中规则不要求的 → 红色FixEntry 会移除
// unknown = 不在 KnownLabels 中的自定义标签 → 黄色警告FixEntry 保留,建议写入规范
var notExpected = currentLbls.Except(expectedLbls, StringComparer.Ordinal);
var extra = notExpected.Where(l => AddressableRules.KnownLabels.Contains(l)).ToArray();
var unknown = notExpected.Where(l => !AddressableRules.KnownLabels.Contains(l)).ToArray();
_reports.Add(new EntryReport _reports.Add(new EntryReport
{ {
@@ -287,20 +307,24 @@ namespace BaseGames.Editor
ExpectedLabels = expectedLbls, ExpectedLabels = expectedLbls,
MissingLabels = missing, MissingLabels = missing,
ExtraLabels = extra, ExtraLabels = extra,
UnknownLabels = unknown,
}); });
} }
} }
// 问题项排前面,正常项排后;同类按 Address 字母序 // 问题项排前面,仅有警告的次之,正常项排后;同类按 Address 字母序
_reports = _reports _reports = _reports
.OrderBy(r => r.IsOk) .OrderBy(r => r.IsOk ? (r.HasWarnings ? 1 : 2) : 0)
.ThenBy(r => r.Address, StringComparer.Ordinal) .ThenBy(r => r.Address, StringComparer.Ordinal)
.ToList(); .ToList();
_scanned = true; _scanned = true;
Repaint(); Repaint();
int issues = _reports.Count(r => !r.IsOk);
int warnings = _reports.Count(r => r.IsOk && r.HasWarnings);
Debug.Log($"[AddressableRuleSync] 扫描完成:{_reports.Count} 个条目," + Debug.Log($"[AddressableRuleSync] 扫描完成:{_reports.Count} 个条目," +
$"{_reports.Count(r => !r.IsOk)} 个需要修复。"); $"{issues} 个需要修复,{warnings} 个含自定义标签警告。");
} }
// ── 修复逻辑 ────────────────────────────────────────────────────────── // ── 修复逻辑 ──────────────────────────────────────────────────────────
@@ -310,6 +334,22 @@ namespace BaseGames.Editor
var issues = _reports.Where(r => !r.IsOk).ToList(); var issues = _reports.Where(r => !r.IsOk).ToList();
if (issues.Count == 0) return; if (issues.Count == 0) return;
int moveCount = issues.Count(r => !r.GroupOk);
int addCount = issues.Sum(r => r.MissingLabels.Length);
int removeCount = issues.Sum(r => r.ExtraLabels.Length);
// 干跑预览对话框
bool confirmed = EditorUtility.DisplayDialog(
"确认修复所有问题",
$"将对 {issues.Count} 个条目执行以下操作:\n\n" +
$" • 移动分组:{moveCount} 个\n" +
$" • 添加标签:{addCount} 个\n" +
$" • 移除多余规则标签:{removeCount} 个\n\n" +
"⚠ 自定义标签(黄色警告项)不会被删除。\n" +
"此操作不可撤销,请确认后继续。",
"确认修复", "取消");
if (!confirmed) return;
int fixedCount = 0; int fixedCount = 0;
foreach (var r in issues) foreach (var r in issues)
{ {
@@ -355,7 +395,8 @@ namespace BaseGames.Editor
changed = true; changed = true;
} }
// 移除多余标签 // 移除多余规则标签ExtraLabels 只包含 KnownLabels 中规则不要求的标签;
// UnknownLabels 是用户自定义标签,刻意保留,不做删除)
foreach (var lbl in r.ExtraLabels) foreach (var lbl in r.ExtraLabels)
{ {
entry.SetLabel(lbl, false, true); entry.SetLabel(lbl, false, true);

View File

@@ -14,23 +14,57 @@ namespace BaseGames.Editor
/// </summary> /// </summary>
public static class AddressableRules public static class AddressableRules
{ {
// ── 已知标签白名单 ────────────────────────────────────────────────────────
// AddressableRuleSyncWindow 用此白名单区分:
// • 规则要求但缺失 → 红色,必须补齐
// • 规则不要求但存在且不在白名单 → 黄色警告(自定义标签,建议写进规范)
// • 规则不要求但存在且在白名单 → 白色,合法的人工附加标签
// 每次向 AddressablesLabelSpec 新增 Label 时,须同步在此处添加。
public static readonly HashSet<string> KnownLabels = new(StringComparer.Ordinal)
{
AddressKeys.Labels.Preload,
AddressKeys.Labels.Poolable,
AddressKeys.Labels.Enemy,
AddressKeys.Labels.BGM,
AddressKeys.Labels.SFX,
AddressKeys.Labels.Charms,
AddressKeys.Labels.Config,
AddressKeys.Labels.Weapon,
};
// ── 前缀 → 分组名 ────────────────────────────────────────────────────── // ── 前缀 → 分组名 ──────────────────────────────────────────────────────
// 规则:按 AssetFolderSpec §8.1 Group 划分策略。 // 规则:按 AssetFolderSpec §8.1 Group 划分策略。
// 顺序:更长/更具体的前缀必须排在更短/更泛化的前缀之前,否则短前缀会先匹配。 // 顺序:更长/更具体的前缀必须排在更短/更泛化的前缀之前,否则短前缀会先匹配。
// 特殊Room_/Boss_ 地址的分组名在运行时动态计算,见 GetExpectedGroup()。 // 特殊Room_/Boss_ 地址的分组名在运行时动态计算,见 GetExpectedGroup()。
public static readonly (string Prefix, string Group)[] PrefixGroupMap = public static readonly (string Prefix, string Group)[] PrefixGroupMap =
{ {
// ── 场景 ─────────────────────────────────────────────────────────
("Scene_", "Scenes"), ("Scene_", "Scenes"),
// ── 玩家 & 武器 ──────────────────────────────────────────────────
("PLY_", "Player"), ("PLY_", "Player"),
("WPN_", "Player"), // 武器与玩家 Prefab 同组AssetFolderSpec §8.1 ("WPN_", "Player"), // 武器与玩家 Prefab 同组AssetFolderSpec §8.1
// ── 敌人 & 投射物 ────────────────────────────────────────────────
("ENM_", "Enemies"), ("ENM_", "Enemies"),
("PROJ_", "Projectiles"), ("PROJ_", "Projectiles"),
("VFX_", "VFX_Common"), // 通用特效组AssetFolderSpec §8.1 // ── 特效 & UI ────────────────────────────────────────────────────
("VFX_", "VFX_Common"), // 通用特效AssetFolderSpec §8.1
("UI_", "UI"), ("UI_", "UI"),
// ── 收集物 ──────────────────────────────────────────────────────
("COL_", "Collectibles"), ("COL_", "Collectibles"),
("CHM_", "Config"), // 护身符 SO 归入 Config 组AddressablesLabelSpec §3.9 // ── 配置数据(更具体的前缀排在泛化前缀之前)──────────────────────
("Config/", "Config"), ("CHM_", "Config"), // 护身符 SOAddressablesLabelSpec §3.9
("AUD_", "Audio_Music"), ("SKL_", "Config"), // 技能配置 SOAssetFolderSpec §4
("SPL_", "Config"), // 法术配置 SO
("ABL_", "Config"), // 能力配置 SO
("MAP_", "Config"), // 地图数据 SOAssetFolderSpec §4
("Config/", "Config"), // 路径前缀配置AssetFolderSpec §8.2
// ── 音频AUD_BGM_ / AUD_SFX_ 必须在通配 AUD_ 之前)─────────────
("AUD_BGM_", "Audio_Music"), // BGM 流式音频
("AUD_SFX_", "Audio_SFX"), // SFX 音效(独立分组便于包体按需加载)
("AUD_", "Audio_Music"), // 未细分音频归 BGM 组
// ── 世界 & 持久化 ────────────────────────────────────────────────
("WLD_", "World"), // 可交互世界物件 Prefab
("SYS_", "Persistent"), // Persistent 场景管理器 Prefab
}; };
// ── 精确地址 → 标签(优先级高于前缀规则)──────────────────────────────── // ── 精确地址 → 标签(优先级高于前缀规则)────────────────────────────────
@@ -51,19 +85,33 @@ namespace BaseGames.Editor
// 顺序更具体的前缀AUD_BGM_在更泛化的前缀AUD_之前。 // 顺序更具体的前缀AUD_BGM_在更泛化的前缀AUD_之前。
private static readonly (string Prefix, string[] Labels)[] PrefixLabelMap = private static readonly (string Prefix, string[] Labels)[] PrefixLabelMap =
{ {
// ── 音频(更具体的 AUD_BGM_ / AUD_SFX_ 必须排在 AUD_ 之前)──────
("AUD_BGM_", new[] { AddressKeys.Labels.BGM }), ("AUD_BGM_", new[] { AddressKeys.Labels.BGM }),
("AUD_SFX_", new[] { AddressKeys.Labels.SFX }), ("AUD_SFX_", new[] { AddressKeys.Labels.SFX }),
("AUD_", new[] { AddressKeys.Labels.BGM }), // 未细分音频默认归 BGM ("AUD_", new[] { AddressKeys.Labels.BGM }), // 未细分音频默认归 BGM
("Scene_", Array.Empty<string>()), // 除 MainMenu 外场景无 label // ── 场景(除 MainMenu 外无 label,由 ExactLabelMap 特殊处理)──────
("Scene_", Array.Empty<string>()),
// ── 玩家 & 武器 ──────────────────────────────────────────────────
("PLY_", new[] { AddressKeys.Labels.Preload }), ("PLY_", new[] { AddressKeys.Labels.Preload }),
("WPN_", new[] { AddressKeys.Labels.Weapon, AddressKeys.Labels.Preload }), ("WPN_", new[] { AddressKeys.Labels.Weapon, AddressKeys.Labels.Preload }),
// ── 敌人 & 投射物 ────────────────────────────────────────────────
("ENM_", new[] { AddressKeys.Labels.Enemy }), ("ENM_", new[] { AddressKeys.Labels.Enemy }),
("PROJ_", new[] { AddressKeys.Labels.Poolable, AddressKeys.Labels.Preload }), ("PROJ_", new[] { AddressKeys.Labels.Poolable, AddressKeys.Labels.Preload }),
// ── 特效 & UI ────────────────────────────────────────────────────
("VFX_", new[] { AddressKeys.Labels.Poolable, AddressKeys.Labels.Preload }), ("VFX_", new[] { AddressKeys.Labels.Poolable, AddressKeys.Labels.Preload }),
("UI_", Array.Empty<string>()), // 除 FloatingDamageText 外 UI 无默认 label ("UI_", Array.Empty<string>()), // 除 FloatingDamageText 外 UI 无默认 label
// ── 收集物 ──────────────────────────────────────────────────────
("COL_", new[] { AddressKeys.Labels.Poolable, AddressKeys.Labels.Preload }), ("COL_", new[] { AddressKeys.Labels.Poolable, AddressKeys.Labels.Preload }),
// ── 配置数据 ─────────────────────────────────────────────────────
("CHM_", new[] { AddressKeys.Labels.Charms }), ("CHM_", new[] { AddressKeys.Labels.Charms }),
("MAP_", new[] { AddressKeys.Labels.Config }), // 地图数据 SO 为动态加载配置
("Config/", new[] { AddressKeys.Labels.Config }), ("Config/", new[] { AddressKeys.Labels.Config }),
// ── 技能 / 法术 / 能力 / 世界物件 / 持久化:无批量加载需求,不加 Label ──
("SKL_", Array.Empty<string>()),
("SPL_", Array.Empty<string>()),
("ABL_", Array.Empty<string>()),
("WLD_", Array.Empty<string>()),
("SYS_", Array.Empty<string>()),
}; };
// ── 公开 API ─────────────────────────────────────────────────────────── // ── 公开 API ───────────────────────────────────────────────────────────