- Implemented WeaponFeedback class for handling weapon-related feedbacks such as hit effects and attack sounds. - Added meta file for AddressableManagerWindow to manage addressable assets. - Included a new jump.data file for profiler data.
1275 lines
59 KiB
C#
1275 lines
59 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 UnityEditor.AddressableAssets.Settings.GroupSchemas;
|
||
using UnityEngine;
|
||
using BaseGames.Core.Assets;
|
||
|
||
namespace BaseGames.Editor
|
||
{
|
||
/// <summary>
|
||
/// Addressables 统一管理工具。
|
||
/// 规范来源:AddressablesLabelSpec.md §3 | AssetFolderSpec.md §8。
|
||
///
|
||
/// 菜单:BaseGames → Addressables → Addressables Manager(总入口)
|
||
/// BaseGames → Addressables → Addressable Batch Tool(直达批量注册 Tab)
|
||
/// BaseGames → Addressables → Rule Sync(直达规则校验 Tab)
|
||
/// </summary>
|
||
public sealed class AddressableManagerWindow : EditorWindow
|
||
{
|
||
// ── Tabs ──────────────────────────────────────────────────────────────
|
||
|
||
private static readonly GUIContent[] TabContents =
|
||
{
|
||
new GUIContent(" 📊 总览 "),
|
||
new GUIContent(" 📦 批量注册 "),
|
||
new GUIContent(" 🔑 键同步 "),
|
||
new GUIContent(" 🔧 规则校验 "),
|
||
};
|
||
|
||
private const int TabDashboard = 0;
|
||
private const int TabRegister = 1;
|
||
private const int TabKeySync = 2;
|
||
private const int TabRuleSync = 3;
|
||
|
||
// ── Dashboard State ───────────────────────────────────────────────────
|
||
|
||
private int _dTotal, _dOk, _dIssue, _dWarn;
|
||
private bool _dReady;
|
||
private string _dTime = "";
|
||
|
||
// ── Register State ────────────────────────────────────────────────────
|
||
|
||
private int _regSrc; // 0=AllGame 1=Folder 2=Selection
|
||
private DefaultAsset _regFolderAsset;
|
||
private string _regSearch = "";
|
||
private bool _regOnlyNew = true;
|
||
private readonly bool[] _regTypeOn = { true, true, true, false, false }; // Prefab/Scene/SO/Audio/Tex
|
||
|
||
private List<RegEntry> _regEntries;
|
||
private Vector2 _regScroll;
|
||
|
||
// ── Key Sync State ────────────────────────────────────────────────────
|
||
|
||
private List<KeyEntry> _keyEntries;
|
||
private bool _keyOnlyMissing = true;
|
||
private Vector2 _keyScroll;
|
||
|
||
// ── Rule Sync State ───────────────────────────────────────────────────
|
||
|
||
private List<RuleEntry> _ruleEntries;
|
||
private bool _ruleShowOk;
|
||
private bool _ruleScanned;
|
||
private string _ruleSearch = "";
|
||
private Vector2 _ruleScroll;
|
||
|
||
// ── Shared Options ────────────────────────────────────────────────────
|
||
|
||
private bool _applyRules = true;
|
||
private bool _overwrite;
|
||
private string _extraLabel = "";
|
||
private string _newGroupName = "";
|
||
|
||
// ── Tab ───────────────────────────────────────────────────────────────
|
||
|
||
[SerializeField] private int _tab;
|
||
|
||
// ── Styles / Colors ───────────────────────────────────────────────────
|
||
|
||
private GUIStyle _sBold, _sLink, _sCard, _sEvenRow, _sCenGrey;
|
||
private bool _stylesReady;
|
||
|
||
private static readonly Color CG = new Color(0.25f, 0.82f, 0.40f); // green OK
|
||
private static readonly Color CY = new Color(0.95f, 0.76f, 0.12f); // yellow warn
|
||
private static readonly Color CR = new Color(0.90f, 0.28f, 0.22f); // red error
|
||
private static readonly Color CD = new Color(0.55f, 0.55f, 0.55f); // dim
|
||
private static readonly Color CE = new Color(0.18f, 0.18f, 0.18f, 0.35f); // even row bg
|
||
|
||
// ── Column widths ─────────────────────────────────────────────────────
|
||
|
||
private const float CW_Path = 272f;
|
||
private const float CW_Addr = 212f;
|
||
private const float CW_Group = 118f;
|
||
private const float CW_Labels = 152f;
|
||
|
||
// ── Menu ──────────────────────────────────────────────────────────────
|
||
|
||
[MenuItem("BaseGames/Addressables/Addressables Manager", priority = 100)]
|
||
public static void Open() => OpenAt(TabDashboard);
|
||
|
||
[MenuItem("BaseGames/Addressables/Addressable Batch Tool", priority = 200)]
|
||
public static void OpenBatch() => OpenAt(TabRegister);
|
||
|
||
[MenuItem("BaseGames/Addressables/Rule Sync", priority = 110)]
|
||
public static void OpenRuleSync() => OpenAt(TabRuleSync);
|
||
|
||
public static void OpenAt(int tab)
|
||
{
|
||
var win = GetWindow<AddressableManagerWindow>("Addressables Manager");
|
||
win.minSize = new Vector2(1060, 600);
|
||
win._tab = tab;
|
||
win.Show();
|
||
win.Focus();
|
||
}
|
||
|
||
// ── Lifecycle ─────────────────────────────────────────────────────────
|
||
|
||
private void OnGUI()
|
||
{
|
||
EnsureStyles();
|
||
|
||
if (AddressableAssetSettingsDefaultObject.Settings == null)
|
||
{
|
||
EditorGUILayout.Space(12);
|
||
EditorGUILayout.HelpBox(
|
||
"Addressable Settings 未初始化。\n" +
|
||
"请先执行:Window → Asset Management → Addressables → Groups → Create Addressables Settings",
|
||
MessageType.Error);
|
||
return;
|
||
}
|
||
|
||
DrawWindowBar();
|
||
EditorGUILayout.Space(2);
|
||
_tab = GUILayout.Toolbar(_tab, TabContents, GUILayout.Height(28));
|
||
EditorGUILayout.Space(4);
|
||
|
||
switch (_tab)
|
||
{
|
||
case TabDashboard: DrawDashboard(); break;
|
||
case TabRegister: DrawRegister(); break;
|
||
case TabKeySync: DrawKeySync(); break;
|
||
case TabRuleSync: DrawRuleSync(); break;
|
||
}
|
||
}
|
||
|
||
// ── Window toolbar ────────────────────────────────────────────────────
|
||
|
||
private void DrawWindowBar()
|
||
{
|
||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||
{
|
||
GUILayout.Label("⚙ Addressables Manager", _sBold, GUILayout.Width(210));
|
||
GUILayout.FlexibleSpace();
|
||
if (GUILayout.Button("Groups 窗口", EditorStyles.toolbarButton, GUILayout.Width(90)))
|
||
EditorApplication.ExecuteMenuItem("Window/Asset Management/Addressables/Groups");
|
||
if (GUILayout.Button("🔍 验证 AddressKeys", EditorStyles.toolbarButton, GUILayout.Width(124)))
|
||
AddressKeyValidator.ValidateAll();
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
// Tab 0 — 总览 (Dashboard)
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
|
||
private void DrawDashboard()
|
||
{
|
||
EditorGUILayout.Space(10);
|
||
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
GUILayout.FlexibleSpace();
|
||
if (GUILayout.Button("⚡ 全量扫描并注册", GUILayout.Width(168), GUILayout.Height(30)))
|
||
{
|
||
_regSrc = 0;
|
||
_regOnlyNew = true;
|
||
ScanRegisterEntries();
|
||
_tab = TabRegister;
|
||
}
|
||
GUILayout.Space(10);
|
||
if (GUILayout.Button("🔧 扫描并修复规则", GUILayout.Width(168), GUILayout.Height(30)))
|
||
{
|
||
RunRuleScan();
|
||
_tab = TabRuleSync;
|
||
}
|
||
GUILayout.FlexibleSpace();
|
||
}
|
||
|
||
EditorGUILayout.Space(14);
|
||
|
||
if (_dReady)
|
||
{
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
GUILayout.FlexibleSpace();
|
||
DrawStatCard("📦 总资产", _dTotal.ToString(), Color.white);
|
||
GUILayout.Space(10);
|
||
DrawStatCard("✅ 符合规范", _dOk.ToString(), CG);
|
||
GUILayout.Space(10);
|
||
DrawStatCard("❌ 需修复", _dIssue.ToString(), _dIssue > 0 ? CR : CG);
|
||
GUILayout.Space(10);
|
||
DrawStatCard("⚠ 自定义标签", _dWarn.ToString(), _dWarn > 0 ? CY : CD);
|
||
GUILayout.FlexibleSpace();
|
||
}
|
||
EditorGUILayout.Space(6);
|
||
GUILayout.Label($"上次扫描:{_dTime}", _sCenGrey);
|
||
}
|
||
|
||
EditorGUILayout.Space(18);
|
||
EditorGUILayout.LabelField("── 推荐工作流 ──", EditorStyles.boldLabel);
|
||
EditorGUILayout.Space(4);
|
||
EditorGUILayout.HelpBox(
|
||
"① 按命名规范(前缀_描述)为新资产命名,放置到正确文件夹\n" +
|
||
"② 在 AddressKeys.cs 中添加对应 const 字符串常量\n" +
|
||
"③「批量注册」→「⚡ 全量扫描 _Game/」→「注册所有未注册项」\n" +
|
||
"④「规则校验」→「▶ 扫描全部」→「✦ 修复所有问题」\n" +
|
||
"⑤ 点击「验证 AddressKeys」确认无遗漏",
|
||
MessageType.None);
|
||
}
|
||
|
||
private void DrawStatCard(string label, string value, Color valueColor)
|
||
{
|
||
using (new EditorGUILayout.VerticalScope(_sCard, GUILayout.Width(138), GUILayout.Height(68)))
|
||
{
|
||
EditorGUILayout.Space(4);
|
||
var prev = GUI.color;
|
||
GUI.color = valueColor;
|
||
GUILayout.Label(value,
|
||
new GUIStyle(EditorStyles.largeLabel)
|
||
{
|
||
fontSize = 26,
|
||
alignment = TextAnchor.MiddleCenter,
|
||
fontStyle = FontStyle.Bold,
|
||
},
|
||
GUILayout.Height(34), GUILayout.ExpandWidth(true));
|
||
GUI.color = prev;
|
||
GUILayout.Label(label, EditorStyles.centeredGreyMiniLabel, GUILayout.ExpandWidth(true));
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
// Tab 1 — 批量注册 (Batch Register)
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
|
||
private void DrawRegister()
|
||
{
|
||
// ── Source bar ────────────────────────────────────────────────────
|
||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||
{
|
||
GUILayout.Label("数据源", EditorStyles.toolbarButton, GUILayout.Width(48));
|
||
|
||
string[] srcLabels = { "⚡ 全量扫描 _Game/", "📁 指定文件夹", "🖱 当前选中" };
|
||
int[] srcWidths = { 130, 100, 86 };
|
||
for (int i = 0; i < 3; i++)
|
||
{
|
||
bool was = _regSrc == i;
|
||
bool now = GUILayout.Toggle(was, srcLabels[i], EditorStyles.toolbarButton,
|
||
GUILayout.Width(srcWidths[i]));
|
||
if (now && !was) { _regSrc = i; _regEntries = null; }
|
||
}
|
||
|
||
GUILayout.FlexibleSpace();
|
||
if (GUILayout.Button("🔄 刷新", EditorStyles.toolbarButton, GUILayout.Width(60)))
|
||
_regEntries = null;
|
||
if (GUILayout.Button("▶ 扫描", EditorStyles.toolbarButton, GUILayout.Width(60)))
|
||
ScanRegisterEntries();
|
||
}
|
||
|
||
// ── Folder picker ─────────────────────────────────────────────────
|
||
if (_regSrc == 1)
|
||
{
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
_regFolderAsset = (DefaultAsset)EditorGUILayout.ObjectField(
|
||
"目标文件夹", _regFolderAsset, typeof(DefaultAsset), false);
|
||
}
|
||
|
||
// ── Type filters + search ─────────────────────────────────────────
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
GUILayout.Label("类型:", EditorStyles.miniLabel, GUILayout.Width(36));
|
||
string[] typeLabels = { "Prefab", "Scene", "SO/Asset", "Audio", "Texture" };
|
||
for (int i = 0; i < typeLabels.Length; i++)
|
||
_regTypeOn[i] = GUILayout.Toggle(_regTypeOn[i], typeLabels[i], "Button", GUILayout.Width(62));
|
||
|
||
GUILayout.Space(10);
|
||
GUILayout.Label("搜索:", EditorStyles.miniLabel, GUILayout.Width(36));
|
||
_regSearch = GUILayout.TextField(_regSearch, GUILayout.Width(160));
|
||
GUILayout.Space(6);
|
||
_regOnlyNew = GUILayout.Toggle(_regOnlyNew, "仅未注册", GUILayout.Width(68));
|
||
GUILayout.FlexibleSpace();
|
||
}
|
||
|
||
EditorGUILayout.Space(2);
|
||
|
||
// ── Table header ──────────────────────────────────────────────────
|
||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||
{
|
||
GUILayout.Label("资产路径", _sBold, GUILayout.Width(CW_Path));
|
||
GUILayout.Label("Addressable 地址", _sBold, GUILayout.Width(CW_Addr));
|
||
GUILayout.Label("期望分组(规则)", _sBold, GUILayout.Width(CW_Group));
|
||
GUILayout.Label("期望标签(规则)", _sBold, GUILayout.Width(CW_Labels));
|
||
GUILayout.Label("状态 / 操作", _sBold);
|
||
}
|
||
|
||
// ── Content ───────────────────────────────────────────────────────
|
||
if (_regEntries == null)
|
||
{
|
||
EditorGUILayout.HelpBox(
|
||
_regSrc == 2
|
||
? "在 Project 窗口选中资产或文件夹,再点击「▶ 扫描」。"
|
||
: "点击「▶ 扫描」加载资产列表。",
|
||
MessageType.Info);
|
||
DrawSharedOptions();
|
||
return;
|
||
}
|
||
|
||
var display = FilterRegEntries(_regEntries);
|
||
|
||
_regScroll = EditorGUILayout.BeginScrollView(_regScroll);
|
||
for (int i = 0; i < display.Count; i++)
|
||
DrawRegRow(display[i], i);
|
||
EditorGUILayout.EndScrollView();
|
||
|
||
// ── Footer ────────────────────────────────────────────────────────
|
||
int newCnt = display.Count(e => !e.Registered);
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
GUILayout.Label(
|
||
$"显示 {display.Count} 个条目,其中 {newCnt} 个未注册",
|
||
EditorStyles.miniLabel);
|
||
GUILayout.FlexibleSpace();
|
||
GUI.enabled = newCnt > 0;
|
||
if (GUILayout.Button($"注册所有未注册项 ({newCnt})", GUILayout.Width(186)))
|
||
{
|
||
RegisterAll(display);
|
||
SaveAssets();
|
||
}
|
||
GUI.enabled = true;
|
||
}
|
||
|
||
DrawSharedOptions();
|
||
}
|
||
|
||
private List<RegEntry> FilterRegEntries(List<RegEntry> src)
|
||
{
|
||
return src
|
||
.Where(e => !_regOnlyNew || !e.Registered)
|
||
.Where(e => string.IsNullOrEmpty(_regSearch)
|
||
|| e.Path.IndexOf(_regSearch, StringComparison.OrdinalIgnoreCase) >= 0
|
||
|| e.Addr.IndexOf(_regSearch, StringComparison.OrdinalIgnoreCase) >= 0)
|
||
.ToList();
|
||
}
|
||
|
||
private void DrawRegRow(RegEntry e, int idx)
|
||
{
|
||
using (new EditorGUILayout.HorizontalScope(
|
||
idx % 2 == 0 ? _sEvenRow : GUIStyle.none, GUILayout.Height(20)))
|
||
{
|
||
string disp = e.Path.Length > 46
|
||
? "…" + e.Path.Substring(e.Path.Length - 43)
|
||
: e.Path;
|
||
if (GUILayout.Button(new GUIContent(disp, e.Path), _sLink, GUILayout.Width(CW_Path)))
|
||
PingAt(e.Path);
|
||
|
||
e.Addr = EditorGUILayout.TextField(e.Addr, GUILayout.Width(CW_Addr));
|
||
GUILayout.Label(e.Group ?? "Default", GUILayout.Width(CW_Group));
|
||
GUILayout.Label(e.Labels, GUILayout.Width(CW_Labels));
|
||
|
||
if (e.Registered)
|
||
Clr("✅ 已注册", CG, GUILayout.Width(90));
|
||
else if (GUILayout.Button("注册", EditorStyles.miniButton, GUILayout.Width(50)))
|
||
{
|
||
RegisterOne(e);
|
||
SaveAssets();
|
||
}
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
// Tab 2 — 键同步 (Key Sync)
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
|
||
private void DrawKeySync()
|
||
{
|
||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||
{
|
||
if (GUILayout.Button("🔄 刷新", EditorStyles.toolbarButton, GUILayout.Width(65)))
|
||
LoadKeyEntries();
|
||
GUILayout.Space(6);
|
||
_keyOnlyMissing = GUILayout.Toggle(
|
||
_keyOnlyMissing, "仅未注册", EditorStyles.toolbarButton, GUILayout.Width(72));
|
||
GUILayout.FlexibleSpace();
|
||
|
||
if (_keyEntries != null)
|
||
{
|
||
int reg = _keyEntries.Count(e => e.Registered);
|
||
GUILayout.Label(
|
||
$"{reg}/{_keyEntries.Count} 已注册",
|
||
EditorStyles.toolbarButton, GUILayout.Width(98));
|
||
}
|
||
|
||
bool canRegAll = _keyEntries?.Any(e => !e.Registered && e.FoundPath != null) == true;
|
||
GUI.enabled = canRegAll;
|
||
if (GUILayout.Button("注册所有已匹配", EditorStyles.toolbarButton, GUILayout.Width(110)))
|
||
{
|
||
RegisterAllMatchedKeys();
|
||
SaveAssets();
|
||
}
|
||
GUI.enabled = true;
|
||
}
|
||
|
||
if (_keyEntries == null) LoadKeyEntries();
|
||
|
||
// ── Table header ──────────────────────────────────────────────────
|
||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||
{
|
||
GUILayout.Label("常量名", _sBold, GUILayout.Width(190));
|
||
GUILayout.Label("地址 Key", _sBold, GUILayout.Width(200));
|
||
GUILayout.Label("期望分组", _sBold, GUILayout.Width(110));
|
||
GUILayout.Label("期望标签", _sBold, GUILayout.Width(148));
|
||
GUILayout.Label("状态 / 资产 / 操作", _sBold);
|
||
}
|
||
|
||
var display = (_keyOnlyMissing
|
||
? _keyEntries.Where(e => !e.Registered)
|
||
: _keyEntries).ToList();
|
||
|
||
_keyScroll = EditorGUILayout.BeginScrollView(_keyScroll);
|
||
for (int i = 0; i < display.Count; i++)
|
||
DrawKeyRow(display[i], i);
|
||
EditorGUILayout.EndScrollView();
|
||
|
||
if (_keyEntries != null)
|
||
{
|
||
int r = _keyEntries.Count(e => e.Registered);
|
||
int m = _keyEntries.Count(e => !e.Registered && e.FoundPath != null);
|
||
int u = _keyEntries.Count(e => !e.Registered && e.FoundPath == null);
|
||
EditorGUILayout.LabelField(
|
||
$"共 {_keyEntries.Count} 个 Key · 已注册 {r} · 已找到待注册 {m} · 未找到 {u}",
|
||
EditorStyles.miniLabel);
|
||
}
|
||
|
||
DrawSharedOptions();
|
||
}
|
||
|
||
private void DrawKeyRow(KeyEntry e, int idx)
|
||
{
|
||
using (new EditorGUILayout.HorizontalScope(
|
||
idx % 2 == 0 ? _sEvenRow : GUIStyle.none, GUILayout.Height(20)))
|
||
{
|
||
GUILayout.Label(e.Field, GUILayout.Width(190));
|
||
GUILayout.Label(e.Key, GUILayout.Width(200));
|
||
GUILayout.Label(AddressableRules.GetExpectedGroup(e.Key) ?? "Default", GUILayout.Width(110));
|
||
GUILayout.Label(FmtLabels(AddressableRules.GetExpectedLabels(e.Key)), GUILayout.Width(148));
|
||
|
||
if (e.Registered)
|
||
{
|
||
Clr("✅ 已注册", CG, GUILayout.Width(75));
|
||
if (GUILayout.Button(
|
||
new GUIContent(e.ExistingPath ?? "—", e.ExistingPath), _sLink))
|
||
PingAt(e.ExistingPath);
|
||
}
|
||
else if (e.FoundPath != null)
|
||
{
|
||
Clr("⚠ 已找到", CY, GUILayout.Width(75));
|
||
GUILayout.Label(e.FoundPath, GUILayout.ExpandWidth(true));
|
||
if (GUILayout.Button("注册", EditorStyles.miniButton, GUILayout.Width(46)))
|
||
{
|
||
RegisterKey(e);
|
||
SaveAssets();
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Clr("❌ 未找到", CR, GUILayout.Width(75));
|
||
e.ManualObj = EditorGUILayout.ObjectField(
|
||
e.ManualObj, typeof(UnityEngine.Object), false);
|
||
if (e.ManualObj != null &&
|
||
GUILayout.Button("注册", EditorStyles.miniButton, GUILayout.Width(46)))
|
||
{
|
||
RegisterKeyManual(e);
|
||
SaveAssets();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
// Tab 3 — 规则校验 (Rule Sync)
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
|
||
private void DrawRuleSync()
|
||
{
|
||
// ── Toolbar ───────────────────────────────────────────────────────
|
||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||
{
|
||
if (GUILayout.Button("▶ 扫描全部", EditorStyles.toolbarButton, GUILayout.Width(76)))
|
||
RunRuleScan();
|
||
|
||
GUILayout.Space(6);
|
||
_ruleShowOk = GUILayout.Toggle(
|
||
_ruleShowOk, "显示正常项", EditorStyles.toolbarButton, GUILayout.Width(76));
|
||
GUILayout.Space(6);
|
||
_ruleSearch = GUILayout.TextField(
|
||
_ruleSearch, EditorStyles.toolbarSearchField, GUILayout.Width(180));
|
||
|
||
GUILayout.FlexibleSpace();
|
||
|
||
if (_ruleScanned && _ruleEntries != null)
|
||
{
|
||
int iss = _ruleEntries.Count(r => !r.Ok);
|
||
int wrn = _ruleEntries.Count(r => r.Ok && r.HasWarn);
|
||
var pc = GUI.color;
|
||
GUI.color = iss > 0 ? CR : CD;
|
||
GUILayout.Label($"❌ {iss}", EditorStyles.toolbarButton, GUILayout.Width(46));
|
||
GUI.color = wrn > 0 ? CY : CD;
|
||
GUILayout.Label($"⚠ {wrn}", EditorStyles.toolbarButton, GUILayout.Width(46));
|
||
GUI.color = pc;
|
||
}
|
||
|
||
bool hasIssues = _ruleEntries?.Any(r => !r.Ok) == true;
|
||
GUI.enabled = _ruleScanned && hasIssues;
|
||
if (GUILayout.Button("✦ 修复所有问题", EditorStyles.toolbarButton, GUILayout.Width(108)))
|
||
FixAll();
|
||
|
||
GUI.enabled = _ruleScanned;
|
||
if (GUILayout.Button("导出 CSV", EditorStyles.toolbarButton, GUILayout.Width(68)))
|
||
ExportCsv();
|
||
GUI.enabled = true;
|
||
}
|
||
|
||
// ── Stats bar ─────────────────────────────────────────────────────
|
||
if (_ruleScanned && _ruleEntries != null)
|
||
{
|
||
int tot = _ruleEntries.Count;
|
||
int ok = _ruleEntries.Count(r => r.Ok);
|
||
int iss = tot - ok;
|
||
int wrn = _ruleEntries.Count(r => r.HasWarn);
|
||
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
GUILayout.Label($"共 {tot} 条目", EditorStyles.miniLabel, GUILayout.Width(72));
|
||
GUILayout.Space(6);
|
||
Clr($"✅ 正常 {ok}", CG);
|
||
GUILayout.Space(8);
|
||
Clr($"❌ 问题 {iss}", iss > 0 ? CR : CG);
|
||
GUILayout.Space(8);
|
||
Clr($"⚠ 自定义标签 {wrn}", wrn > 0 ? CY : CD);
|
||
GUILayout.FlexibleSpace();
|
||
}
|
||
}
|
||
|
||
// ── Table header ──────────────────────────────────────────────────
|
||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||
{
|
||
GUILayout.Label("Address", _sBold, GUILayout.Width(210));
|
||
GUILayout.Label("当前分组", _sBold, GUILayout.Width(110));
|
||
GUILayout.Label("期望分组", _sBold, GUILayout.Width(110));
|
||
GUILayout.Label("缺失标签", _sBold, GUILayout.Width(118));
|
||
GUILayout.Label("多余规则标签", _sBold, GUILayout.Width(108));
|
||
GUILayout.Label("自定义标签", _sBold, GUILayout.Width(100));
|
||
GUILayout.Label("状态", _sBold);
|
||
}
|
||
|
||
// ── Rows ──────────────────────────────────────────────────────────
|
||
_ruleScroll = EditorGUILayout.BeginScrollView(_ruleScroll);
|
||
if (!_ruleScanned)
|
||
{
|
||
EditorGUILayout.HelpBox(
|
||
"点击「▶ 扫描全部」分析已注册资产与规范的差异。", MessageType.Info);
|
||
}
|
||
else
|
||
{
|
||
var show = _ruleEntries
|
||
.Where(r => _ruleShowOk || !r.Ok)
|
||
.Where(r => string.IsNullOrEmpty(_ruleSearch)
|
||
|| r.Address.IndexOf(_ruleSearch, StringComparison.OrdinalIgnoreCase) >= 0)
|
||
.ToList();
|
||
|
||
if (show.Count == 0)
|
||
EditorGUILayout.HelpBox("✅ 所有已注册资产均符合规范!", MessageType.Info);
|
||
|
||
for (int i = 0; i < show.Count; i++)
|
||
DrawRuleRow(show[i], i);
|
||
}
|
||
EditorGUILayout.EndScrollView();
|
||
|
||
// ── Footer hint ───────────────────────────────────────────────────
|
||
EditorGUILayout.HelpBox(
|
||
"规则来源:AddressablesLabelSpec.md §3 | 分组规则:AssetFolderSpec.md §8.1\n" +
|
||
"「修复所有问题」修正分组与标签;不注册新资产;不删除自定义标签(⚠ 黄色)。",
|
||
MessageType.None);
|
||
}
|
||
|
||
private void DrawRuleRow(RuleEntry r, int idx)
|
||
{
|
||
using (new EditorGUILayout.HorizontalScope(
|
||
idx % 2 == 0 ? _sEvenRow : GUIStyle.none, GUILayout.Height(20)))
|
||
{
|
||
if (GUILayout.Button(r.Address, _sLink, GUILayout.Width(210)))
|
||
PingAt(r.AssetPath);
|
||
|
||
Clr(r.CurGroup ?? "—", r.GroupOk ? CG : CR, GUILayout.Width(110));
|
||
GUILayout.Label(r.ExpGroup ?? "(未覆盖)", GUILayout.Width(110));
|
||
Clr(r.Missing.Length > 0 ? Jn(r.Missing) : "—",
|
||
r.Missing.Length > 0 ? CR : CG, GUILayout.Width(118));
|
||
Clr(r.Extra.Length > 0 ? Jn(r.Extra) : "—",
|
||
r.Extra.Length > 0 ? CR : CD, GUILayout.Width(108));
|
||
Clr(r.Unknown.Length > 0 ? Jn(r.Unknown) : "—",
|
||
r.Unknown.Length > 0 ? CY : CD, GUILayout.Width(100));
|
||
|
||
if (r.Ok)
|
||
Clr(r.HasWarn ? "⚠ 自定义标签" : "✅ 正常", r.HasWarn ? CY : CG);
|
||
else
|
||
{
|
||
Clr("❌ 需修复", CR, GUILayout.Width(62));
|
||
if (GUILayout.Button("修复", EditorStyles.miniButton, GUILayout.Width(40)))
|
||
{
|
||
FixOne(r);
|
||
SaveAssets();
|
||
RunRuleScan();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Shared Options ────────────────────────────────────────────────────
|
||
|
||
private void DrawSharedOptions()
|
||
{
|
||
EditorGUILayout.Space(4);
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
_applyRules = GUILayout.Toggle(_applyRules, "自动应用分组/标签规则");
|
||
GUILayout.Space(14);
|
||
_overwrite = GUILayout.Toggle(_overwrite, "覆盖已有地址");
|
||
GUILayout.Space(14);
|
||
GUILayout.Label("附加标签:", GUILayout.Width(58));
|
||
_extraLabel = GUILayout.TextField(_extraLabel, GUILayout.Width(90));
|
||
GUILayout.Space(14);
|
||
GUILayout.Label("新建分组:", GUILayout.Width(58));
|
||
_newGroupName = GUILayout.TextField(_newGroupName, GUILayout.Width(110));
|
||
if (GUILayout.Button("创建", EditorStyles.miniButton, GUILayout.Width(40))
|
||
&& !string.IsNullOrWhiteSpace(_newGroupName))
|
||
{
|
||
var s = AddressableAssetSettingsDefaultObject.Settings;
|
||
if (s != null)
|
||
{
|
||
EnsureGroup(s, _newGroupName.Trim());
|
||
SaveAssets();
|
||
}
|
||
}
|
||
GUILayout.FlexibleSpace();
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
// Register Logic
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
|
||
private void ScanRegisterEntries()
|
||
{
|
||
_regEntries = new List<RegEntry>();
|
||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||
if (settings == null) return;
|
||
|
||
var regGuids = CollectAllGuids(settings);
|
||
var files = GatherFiles();
|
||
|
||
try
|
||
{
|
||
for (int i = 0; i < files.Count; i++)
|
||
{
|
||
if (i % 20 == 0)
|
||
EditorUtility.DisplayProgressBar(
|
||
"扫描资产", Path.GetFileName(files[i]), (float)i / files.Count);
|
||
|
||
string p = files[i];
|
||
if (!IsManageableType(p) || ShouldExclude(p) || !PassesTypeFilter(p)) continue;
|
||
|
||
string guid = AssetDatabase.AssetPathToGUID(p);
|
||
if (string.IsNullOrEmpty(guid)) continue;
|
||
|
||
string addr = BuildAddr(p);
|
||
_regEntries.Add(new RegEntry
|
||
{
|
||
Path = p,
|
||
Guid = guid,
|
||
Addr = addr,
|
||
Registered = regGuids.Contains(guid),
|
||
Group = AddressableRules.GetExpectedGroup(addr),
|
||
Labels = FmtLabels(AddressableRules.GetExpectedLabels(addr)),
|
||
});
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
EditorUtility.ClearProgressBar();
|
||
}
|
||
|
||
_regEntries = _regEntries
|
||
.GroupBy(e => e.Guid)
|
||
.Select(g => g.First())
|
||
.OrderBy(e => e.Registered ? 1 : 0)
|
||
.ThenBy(e => e.Addr)
|
||
.ToList();
|
||
}
|
||
|
||
private List<string> GatherFiles()
|
||
{
|
||
var result = new List<string>();
|
||
|
||
if (_regSrc == 0)
|
||
{
|
||
foreach (string g in AssetDatabase.FindAssets("t:Object", new[] { "Assets/_Game" }))
|
||
{
|
||
string p = AssetDatabase.GUIDToAssetPath(g);
|
||
if (!AssetDatabase.IsValidFolder(p)) result.Add(p);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
if (_regSrc == 1)
|
||
{
|
||
string fp = _regFolderAsset != null
|
||
? AssetDatabase.GetAssetPath(_regFolderAsset)
|
||
: "";
|
||
if (string.IsNullOrEmpty(fp) || !AssetDatabase.IsValidFolder(fp))
|
||
return result;
|
||
foreach (string g in AssetDatabase.FindAssets("t:Object", new[] { fp }))
|
||
{
|
||
string p = AssetDatabase.GUIDToAssetPath(g);
|
||
if (!AssetDatabase.IsValidFolder(p)) result.Add(p);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// Selection
|
||
foreach (string guid in Selection.assetGUIDs)
|
||
{
|
||
string p = AssetDatabase.GUIDToAssetPath(guid);
|
||
if (AssetDatabase.IsValidFolder(p))
|
||
{
|
||
foreach (string sg in AssetDatabase.FindAssets("t:Object", new[] { p }))
|
||
{
|
||
string sp = AssetDatabase.GUIDToAssetPath(sg);
|
||
if (!AssetDatabase.IsValidFolder(sp)) result.Add(sp);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
result.Add(p);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
private bool PassesTypeFilter(string p)
|
||
{
|
||
bool anyOn = _regTypeOn.Any(v => v);
|
||
if (!anyOn) return true;
|
||
|
||
string ext = Path.GetExtension(p).ToLowerInvariant();
|
||
if (_regTypeOn[0] && ext == ".prefab") return true;
|
||
if (_regTypeOn[1] && ext == ".unity") return true;
|
||
if (_regTypeOn[2] && ext == ".asset") return true;
|
||
if (_regTypeOn[3] && (ext == ".mp3" || ext == ".wav" || ext == ".ogg")) return true;
|
||
if (_regTypeOn[4] && (ext == ".png" || ext == ".jpg" || ext == ".tga")) return true;
|
||
return false;
|
||
}
|
||
|
||
private void RegisterOne(RegEntry e)
|
||
{
|
||
if (e.Registered && !_overwrite) return;
|
||
var s = AddressableAssetSettingsDefaultObject.Settings;
|
||
if (s == null) return;
|
||
|
||
if (!ConfirmAddressConflict(s, e.Addr, e.Guid)) return;
|
||
|
||
var grp = _applyRules && e.Group != null ? EnsureGroup(s, e.Group) : s.DefaultGroup;
|
||
var entry = s.FindAssetEntry(e.Guid)
|
||
?? s.CreateOrMoveEntry(e.Guid, grp, false, false);
|
||
if (entry == null) return;
|
||
|
||
entry.address = e.Addr;
|
||
s.MoveEntry(entry, grp, false, false);
|
||
|
||
if (_applyRules)
|
||
foreach (var lbl in AddressableRules.GetExpectedLabels(e.Addr))
|
||
SetLabel(s, entry, lbl);
|
||
|
||
if (!string.IsNullOrWhiteSpace(_extraLabel))
|
||
SetLabel(s, entry, _extraLabel.Trim());
|
||
|
||
e.Registered = true;
|
||
}
|
||
|
||
private void RegisterAll(List<RegEntry> entries)
|
||
{
|
||
int cnt = 0;
|
||
foreach (var e in entries.Where(e => !e.Registered))
|
||
{
|
||
RegisterOne(e);
|
||
cnt++;
|
||
}
|
||
Debug.Log($"[AddressablesManager] 批量注册完成:{cnt} 个资产");
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
// Key Sync Logic
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
|
||
private void LoadKeyEntries()
|
||
{
|
||
_keyEntries = new List<KeyEntry>();
|
||
var s = AddressableAssetSettingsDefaultObject.Settings;
|
||
if (s == null) return;
|
||
|
||
// Build address → path map from all registered entries
|
||
var addrMap = new Dictionary<string, string>(StringComparer.Ordinal);
|
||
foreach (var g in s.groups)
|
||
if (g != null)
|
||
foreach (var e in g.entries)
|
||
if (e != null) addrMap[e.address] = e.AssetPath;
|
||
|
||
var fields = typeof(AddressKeys)
|
||
.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)
|
||
.Where(f => f.IsLiteral && !f.IsInitOnly && f.FieldType == typeof(string));
|
||
|
||
foreach (var f in fields)
|
||
{
|
||
var key = (string)f.GetRawConstantValue();
|
||
var ke = new KeyEntry { Field = f.Name, Key = key };
|
||
|
||
if (addrMap.TryGetValue(key, out string ep))
|
||
{
|
||
ke.Registered = true;
|
||
ke.ExistingPath = ep;
|
||
}
|
||
else
|
||
{
|
||
// Auto-search by the last segment of the key (handles "Config/Name")
|
||
string searchTerm = key.Contains('/')
|
||
? key.Substring(key.LastIndexOf('/') + 1)
|
||
: key;
|
||
|
||
string[] guids = AssetDatabase.FindAssets(searchTerm);
|
||
string best = guids
|
||
.Select(AssetDatabase.GUIDToAssetPath)
|
||
.Where(p => !AssetDatabase.IsValidFolder(p) && IsManageableType(p))
|
||
.OrderBy(p => Path.GetFileNameWithoutExtension(p) == searchTerm ? 0 : 1)
|
||
.FirstOrDefault();
|
||
|
||
if (best != null)
|
||
{
|
||
ke.FoundPath = best;
|
||
ke.FoundGuid = AssetDatabase.AssetPathToGUID(best);
|
||
}
|
||
}
|
||
|
||
_keyEntries.Add(ke);
|
||
}
|
||
}
|
||
|
||
private void RegisterKey(KeyEntry e)
|
||
{
|
||
if (e.FoundGuid == null) return;
|
||
string grp = _applyRules ? AddressableRules.GetExpectedGroup(e.Key) : null;
|
||
DoRegister(e.FoundGuid, e.Key, grp);
|
||
e.Registered = true;
|
||
e.ExistingPath = e.FoundPath;
|
||
e.FoundPath = null;
|
||
}
|
||
|
||
private void RegisterKeyManual(KeyEntry e)
|
||
{
|
||
string p = AssetDatabase.GetAssetPath(e.ManualObj);
|
||
string guid = AssetDatabase.AssetPathToGUID(p);
|
||
string grp = _applyRules ? AddressableRules.GetExpectedGroup(e.Key) : null;
|
||
DoRegister(guid, e.Key, grp);
|
||
e.Registered = true;
|
||
e.ExistingPath = p;
|
||
e.ManualObj = null;
|
||
}
|
||
|
||
private void RegisterAllMatchedKeys()
|
||
{
|
||
int cnt = 0;
|
||
foreach (var e in _keyEntries.Where(e => !e.Registered && e.FoundPath != null))
|
||
{
|
||
RegisterKey(e);
|
||
cnt++;
|
||
}
|
||
Debug.Log($"[AddressablesManager] 键同步完成:注册 {cnt} 个 Key");
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
// Rule Sync Logic
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
|
||
private void RunRuleScan()
|
||
{
|
||
_ruleEntries = new List<RuleEntry>();
|
||
var s = AddressableAssetSettingsDefaultObject.Settings;
|
||
if (s == null) return;
|
||
|
||
foreach (var grp in s.groups)
|
||
{
|
||
if (grp == null) continue;
|
||
foreach (var e in grp.entries)
|
||
{
|
||
if (e == null) continue;
|
||
|
||
var expG = AddressableRules.GetExpectedGroup(e.address);
|
||
var expL = AddressableRules.GetExpectedLabels(e.address);
|
||
var curL = e.labels.ToArray();
|
||
var notExp = curL.Except(expL, StringComparer.Ordinal).ToArray();
|
||
|
||
_ruleEntries.Add(new RuleEntry
|
||
{
|
||
Address = e.address,
|
||
AssetPath = e.AssetPath,
|
||
CurGroup = grp.name,
|
||
ExpGroup = expG,
|
||
Missing = expL.Except(curL, StringComparer.Ordinal).ToArray(),
|
||
Extra = notExp.Where(l => AddressableRules.KnownLabels.Contains(l)).ToArray(),
|
||
Unknown = notExp.Where(l => !AddressableRules.KnownLabels.Contains(l)).ToArray(),
|
||
});
|
||
}
|
||
}
|
||
|
||
_ruleEntries = _ruleEntries
|
||
.OrderBy(r => r.Ok ? 1 : 0)
|
||
.ThenBy(r => r.Address, StringComparer.Ordinal)
|
||
.ToList();
|
||
|
||
_ruleScanned = true;
|
||
|
||
// Sync dashboard counters
|
||
_dTotal = _ruleEntries.Count;
|
||
_dOk = _ruleEntries.Count(r => r.Ok);
|
||
_dIssue = _ruleEntries.Count(r => !r.Ok);
|
||
_dWarn = _ruleEntries.Count(r => r.HasWarn);
|
||
_dReady = true;
|
||
_dTime = DateTime.Now.ToString("HH:mm:ss");
|
||
|
||
Debug.Log(
|
||
$"[AddressablesManager] 规则扫描:{_ruleEntries.Count} 条 · " +
|
||
$"{_dIssue} 需修复 · {_dWarn} 含自定义标签");
|
||
Repaint();
|
||
}
|
||
|
||
private void FixAll()
|
||
{
|
||
var issues = _ruleEntries.Where(r => !r.Ok).ToList();
|
||
if (issues.Count == 0) return;
|
||
|
||
int moves = issues.Count(r => !r.GroupOk);
|
||
int adds = issues.Sum(r => r.Missing.Length);
|
||
int rems = issues.Sum(r => r.Extra.Length);
|
||
|
||
if (!EditorUtility.DisplayDialog("确认修复所有问题",
|
||
$"即将对 {issues.Count} 个条目执行:\n\n" +
|
||
$" • 移动分组:{moves} 个\n" +
|
||
$" • 添加标签:{adds} 个\n" +
|
||
$" • 移除多余规则标签:{rems} 个\n\n" +
|
||
"⚠ 自定义标签(黄色⚠)不会被删除。此操作不可撤销。",
|
||
"确认修复", "取消"))
|
||
return;
|
||
|
||
int cnt = 0;
|
||
foreach (var r in issues)
|
||
if (FixOne(r)) cnt++;
|
||
|
||
SaveAssets();
|
||
RunRuleScan();
|
||
Debug.Log($"[AddressablesManager] 修复完成:共处理 {cnt} 个条目");
|
||
}
|
||
|
||
private bool FixOne(RuleEntry r)
|
||
{
|
||
var s = AddressableAssetSettingsDefaultObject.Settings;
|
||
if (s == null) return false;
|
||
|
||
var entry = FindByAddr(s, r.Address);
|
||
if (entry == null) return false;
|
||
|
||
bool changed = false;
|
||
|
||
if (!r.GroupOk && r.ExpGroup != null)
|
||
{
|
||
var grp = EnsureGroup(s, r.ExpGroup);
|
||
if (grp != null && entry.parentGroup != grp)
|
||
{
|
||
s.MoveEntry(entry, grp, false, false);
|
||
changed = true;
|
||
}
|
||
}
|
||
|
||
foreach (var lbl in r.Missing)
|
||
{
|
||
SetLabel(s, entry, lbl);
|
||
changed = true;
|
||
}
|
||
|
||
foreach (var lbl in r.Extra)
|
||
{
|
||
entry.SetLabel(lbl, false, true);
|
||
changed = true;
|
||
}
|
||
|
||
return changed;
|
||
}
|
||
|
||
private void ExportCsv()
|
||
{
|
||
if (_ruleEntries == null) return;
|
||
string path = EditorUtility.SaveFilePanel(
|
||
"导出规则报告", "", "AddressableRuleReport.csv", "csv");
|
||
if (string.IsNullOrEmpty(path)) return;
|
||
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine("Address,CurrentGroup,ExpectedGroup,GroupOk,Missing,Extra,Unknown,Status");
|
||
foreach (var r in _ruleEntries)
|
||
sb.AppendLine(
|
||
$"\"{r.Address}\",\"{r.CurGroup}\",\"{r.ExpGroup ?? ""}\"," +
|
||
$"{r.GroupOk},\"{Jn(r.Missing)}\",\"{Jn(r.Extra)}\",\"{Jn(r.Unknown)}\"," +
|
||
$"{(r.Ok ? "OK" : "ISSUE")}");
|
||
|
||
File.WriteAllText(path, sb.ToString(), Encoding.UTF8);
|
||
Debug.Log($"[AddressablesManager] CSV 已导出:{path}");
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
// Core Register Primitive
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
|
||
private void DoRegister(string guid, string addr, string groupName)
|
||
{
|
||
var s = AddressableAssetSettingsDefaultObject.Settings;
|
||
if (s == null || string.IsNullOrEmpty(guid)) return;
|
||
|
||
if (!ConfirmAddressConflict(s, addr, guid)) return;
|
||
|
||
var grp = groupName != null ? EnsureGroup(s, groupName) : s.DefaultGroup;
|
||
var entry = s.FindAssetEntry(guid)
|
||
?? s.CreateOrMoveEntry(guid, grp, false, false);
|
||
if (entry == null) return;
|
||
|
||
entry.address = addr;
|
||
s.MoveEntry(entry, grp, false, false);
|
||
|
||
if (_applyRules)
|
||
foreach (var lbl in AddressableRules.GetExpectedLabels(addr))
|
||
SetLabel(s, entry, lbl);
|
||
|
||
if (!string.IsNullOrWhiteSpace(_extraLabel))
|
||
SetLabel(s, entry, _extraLabel.Trim());
|
||
}
|
||
|
||
private static bool ConfirmAddressConflict(AddressableAssetSettings s, string addr, string guid)
|
||
{
|
||
var dup = FindByAddr(s, addr);
|
||
if (dup == null || dup.guid == guid) return true;
|
||
|
||
return EditorUtility.DisplayDialog(
|
||
"⚠ 地址冲突",
|
||
$"地址 \"{addr}\" 已绑定:\n{dup.AssetPath}\n\n继续将覆盖原绑定。",
|
||
"继续", "取消");
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
// Helpers — Asset Discovery
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
|
||
private static bool IsManageableType(string p)
|
||
{
|
||
string ext = Path.GetExtension(p).ToLowerInvariant();
|
||
return ext is ".prefab" or ".unity" or ".asset"
|
||
or ".png" or ".jpg" or ".tga"
|
||
or ".mp3" or ".wav" or ".ogg"
|
||
or ".controller";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 不应注册为 Addressable 的资产(由 Prefab 依赖链加载,或仅供编辑器引用)。
|
||
/// </summary>
|
||
private static bool ShouldExclude(string p)
|
||
{
|
||
string lp = p.Replace('\\', '/').ToLowerInvariant();
|
||
string name = Path.GetFileNameWithoutExtension(p);
|
||
string ext = Path.GetExtension(p).ToLowerInvariant();
|
||
|
||
if (lp.Contains("/scenes/testings/")) return true;
|
||
if (name.StartsWith("EVT_", StringComparison.Ordinal)) return true;
|
||
if (ext == ".spriteatlas") return true;
|
||
if (ext == ".mat") return true;
|
||
// ENV_* Prefab 由场景直接引用,不注册 Addressable(AssetFolderSpec §4.1)
|
||
if (name.StartsWith("ENV_", StringComparison.OrdinalIgnoreCase)) return true;
|
||
// Build_* 是不符合规范命名的装饰性环境 Prefab,不纳入 Addressables 管理
|
||
if (name.StartsWith("Build_", StringComparison.OrdinalIgnoreCase)) return true;
|
||
// HitBox / HurtBox 子 Prefab 不单独注册(名称中含关键词即排除)
|
||
if (name.IndexOf("HitBox", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||
if (name.IndexOf("HurtBox", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从资产路径推导 Addressable 地址。
|
||
/// 有已知前缀 → 直接用文件名;Data 文件夹内无前缀 SO → "Config/{name}"。
|
||
/// </summary>
|
||
private static string BuildAddr(string p)
|
||
{
|
||
string name = Path.GetFileNameWithoutExtension(p);
|
||
|
||
foreach (var (prefix, _) in AddressableRules.PrefixGroupMap)
|
||
{
|
||
if (prefix.EndsWith('/')) continue; // "Config/" 是地址前缀,不是文件名前缀
|
||
if (name.StartsWith(prefix, StringComparison.Ordinal))
|
||
return name;
|
||
}
|
||
|
||
// Room_ / Boss_ 动态分组(不在 PrefixGroupMap 中,但地址就是文件名)
|
||
if (name.StartsWith("Room_", StringComparison.Ordinal)) return name;
|
||
if (name.StartsWith("Boss_", StringComparison.Ordinal)) return name;
|
||
|
||
// Data / Config 文件夹内无标准前缀的 SO → Config/Name
|
||
string np = p.Replace('\\', '/');
|
||
if ((np.Contains("/_Game/Data/") || np.Contains("/_Game/Config/"))
|
||
&& Path.GetExtension(p).ToLowerInvariant() == ".asset")
|
||
return $"Config/{name}";
|
||
|
||
return name;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
// Helpers — Addressables API
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
|
||
private static HashSet<string> CollectAllGuids(AddressableAssetSettings s)
|
||
{
|
||
var set = new HashSet<string>(StringComparer.Ordinal);
|
||
foreach (var g in s.groups)
|
||
if (g != null)
|
||
foreach (var e in g.entries)
|
||
if (e != null) set.Add(e.guid);
|
||
return set;
|
||
}
|
||
|
||
private static AddressableAssetEntry FindByAddr(AddressableAssetSettings s, string addr)
|
||
{
|
||
foreach (var g in s.groups)
|
||
if (g != null)
|
||
foreach (var e in g.entries)
|
||
if (e?.address == addr) return e;
|
||
return null;
|
||
}
|
||
|
||
private static AddressableAssetGroup EnsureGroup(AddressableAssetSettings s, string name)
|
||
{
|
||
var existing = s.groups.FirstOrDefault(g => g?.name == name);
|
||
if (existing != null) return existing;
|
||
|
||
var tmpl = s.GroupTemplateObjects.FirstOrDefault() as AddressableAssetGroupTemplate;
|
||
var schemas = tmpl != null
|
||
? new List<AddressableAssetGroupSchema>(tmpl.SchemaObjects)
|
||
: null;
|
||
var created = s.CreateGroup(name, false, false, true, schemas);
|
||
if (created != null)
|
||
Debug.Log($"[AddressablesManager] 已自动创建分组:{name}");
|
||
return created ?? s.DefaultGroup;
|
||
}
|
||
|
||
private static void SetLabel(AddressableAssetSettings s, AddressableAssetEntry entry, string label)
|
||
{
|
||
if (!s.GetLabels().Contains(label))
|
||
{
|
||
s.AddLabel(label, true);
|
||
Debug.Log($"[AddressablesManager] 已创建标签:{label}");
|
||
}
|
||
entry.SetLabel(label, true, true);
|
||
}
|
||
|
||
private static void SaveAssets()
|
||
{
|
||
AssetDatabase.SaveAssets();
|
||
AddressableAssetSettingsDefaultObject.Settings?.SetDirty(
|
||
AddressableAssetSettings.ModificationEvent.EntryModified, null, true);
|
||
}
|
||
|
||
private static void PingAt(string assetPath)
|
||
{
|
||
if (string.IsNullOrEmpty(assetPath)) return;
|
||
var obj = AssetDatabase.LoadMainAssetAtPath(assetPath);
|
||
if (obj != null) EditorGUIUtility.PingObject(obj);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
// Helpers — Formatting / UI
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
|
||
private static string FmtLabels(string[] labels)
|
||
=> labels.Length == 0 ? "—" : string.Join(", ", labels);
|
||
|
||
private static string Jn(string[] arr)
|
||
=> arr is { Length: > 0 } ? string.Join("; ", arr) : "";
|
||
|
||
private void Clr(string text, Color c, params GUILayoutOption[] opts)
|
||
{
|
||
var prev = GUI.color;
|
||
GUI.color = c;
|
||
GUILayout.Label(text, opts);
|
||
GUI.color = prev;
|
||
}
|
||
|
||
// ── Styles ────────────────────────────────────────────────────────────
|
||
|
||
private void EnsureStyles()
|
||
{
|
||
if (_stylesReady) return;
|
||
|
||
_sBold = new GUIStyle(EditorStyles.boldLabel) { fontSize = 11 };
|
||
_sLink = new GUIStyle(EditorStyles.linkLabel);
|
||
_sCenGrey = new GUIStyle(EditorStyles.centeredGreyMiniLabel);
|
||
_sCard = new GUIStyle("HelpBox");
|
||
_sEvenRow = new GUIStyle { normal = { background = MkTex(CE) } };
|
||
|
||
_stylesReady = true;
|
||
}
|
||
|
||
private static Texture2D MkTex(Color c)
|
||
{
|
||
var t = new Texture2D(1, 1, TextureFormat.RGBA32, false);
|
||
t.SetPixel(0, 0, c);
|
||
t.Apply();
|
||
return t;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
// Data Types
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
|
||
private class RegEntry
|
||
{
|
||
public string Path, Guid, Addr, Group, Labels;
|
||
public bool Registered;
|
||
}
|
||
|
||
private class KeyEntry
|
||
{
|
||
public string Field, Key;
|
||
public bool Registered;
|
||
public string ExistingPath, FoundPath, FoundGuid;
|
||
public UnityEngine.Object ManualObj;
|
||
}
|
||
|
||
private class RuleEntry
|
||
{
|
||
public string Address, AssetPath, CurGroup, ExpGroup;
|
||
public string[] Missing = Array.Empty<string>();
|
||
public string[] Extra = Array.Empty<string>();
|
||
public string[] Unknown = Array.Empty<string>();
|
||
|
||
public bool GroupOk => ExpGroup == null || CurGroup == ExpGroup;
|
||
public bool LabelsOk => Missing.Length == 0 && Extra.Length == 0;
|
||
public bool Ok => GroupOk && LabelsOk;
|
||
public bool HasWarn => Unknown.Length > 0;
|
||
}
|
||
}
|
||
}
|