Files
zeling_v2/Assets/_Game/Scripts/Editor/Addressables/AddressableManagerWindow.cs
Joywayer b7baf7ad6a Add WeaponFeedback component and AddressableManagerWindow meta file
- 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.
2026-05-22 22:03:32 +08:00

1275 lines
59 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 由场景直接引用,不注册 AddressableAssetFolderSpec §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;
}
}
}