feat: Addressables rules/sync tools, UI fixes, AddressKeys update
- Add AddressableRules.cs: single source of truth for prefix->group and prefix->label rules - Add AddressableRuleSyncWindow.cs: scan/fix/export-CSV tool (BaseGames > Addressables > Rule Sync) - AddressableBatchTool.cs: delegate DeriveGroupName to AddressableRules, remove duplicate PrefixGroupMap - AddressKeys.cs: add Labels constants (Preload, Poolable, Enemy, BGM, SFX, Charms, Config, Weapon) - Docs/Standards/AddressablesLabelSpec.md: new label naming & assignment spec - Docs/Standards/AssetFolderSpec.md: update Addressables group strategy section - SplashScreenController.cs: fix MainMenu loading flow - BootFlowSetupWizard.cs / SceneScaffoldTools.cs: scene scaffold fixes - PlayerInputActions: set UI/Point to Pass-Through type - Persistent.unity: add BootSequencer to auto-load MainMenu on play - EditorBuildSettings.asset: register scenes for build Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,486 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEditor.AddressableAssets;
|
||||
using UnityEditor.AddressableAssets.Settings;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Addressable 规则同步窗口。
|
||||
///
|
||||
/// 功能:
|
||||
/// 1. 扫描所有已注册的 Addressable 资产
|
||||
/// 2. 根据 <see cref="AddressableRules"/> 中的规则计算期望分组与期望标签
|
||||
/// 3. 对比实际值,显示所有不符合规范的条目(分组错误 / 标签缺失 / 标签多余)
|
||||
/// 4. 一键自动修复全部问题
|
||||
/// 5. 导出 CSV 报告供存档或 Code Review
|
||||
///
|
||||
/// 菜单:BaseGames → Addressables → Rule Sync
|
||||
/// </summary>
|
||||
public class AddressableRuleSyncWindow : EditorWindow
|
||||
{
|
||||
// ── 内部数据结构 ───────────────────────────────────────────────────────
|
||||
|
||||
private enum IssueKind { None, WrongGroup, MissingLabel, ExtraLabel }
|
||||
|
||||
private class EntryReport
|
||||
{
|
||||
public string Address;
|
||||
public string AssetPath;
|
||||
public string CurrentGroup;
|
||||
public string ExpectedGroup; // null = 规则未覆盖,维持现状
|
||||
public string[] CurrentLabels;
|
||||
public string[] ExpectedLabels;
|
||||
public string[] MissingLabels; // 应有但没有
|
||||
public string[] ExtraLabels; // 有但不应有
|
||||
public bool GroupOk => ExpectedGroup == null || CurrentGroup == ExpectedGroup;
|
||||
public bool LabelsOk => MissingLabels.Length == 0 && ExtraLabels.Length == 0;
|
||||
public bool IsOk => GroupOk && LabelsOk;
|
||||
}
|
||||
|
||||
// ── 状态 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private List<EntryReport> _reports = new();
|
||||
private Vector2 _scrollPos;
|
||||
private bool _showOk = false;
|
||||
private bool _scanned = false;
|
||||
private string _searchFilter = "";
|
||||
|
||||
// ── 样式(惰性初始化)────────────────────────────────────────────────
|
||||
|
||||
private GUIStyle _okStyle;
|
||||
private GUIStyle _warnStyle;
|
||||
private GUIStyle _errorStyle;
|
||||
private GUIStyle _boldStyle;
|
||||
private GUIStyle _rowEven;
|
||||
private GUIStyle _rowOdd;
|
||||
private bool _stylesReady;
|
||||
|
||||
// ── 颜色 ─────────────────────────────────────────────────────────────
|
||||
|
||||
private static readonly Color ColOk = new(0.20f, 0.78f, 0.35f, 1f);
|
||||
private static readonly Color ColWarn = new(0.95f, 0.75f, 0.10f, 1f);
|
||||
private static readonly Color ColError = new(0.90f, 0.25f, 0.20f, 1f);
|
||||
private static readonly Color ColRowEven = new(0.22f, 0.22f, 0.22f, 0.4f);
|
||||
|
||||
// ── 菜单入口 ──────────────────────────────────────────────────────────
|
||||
|
||||
[MenuItem("BaseGames/Addressables/Rule Sync", priority = 110)]
|
||||
public static void OpenWindow()
|
||||
{
|
||||
var win = GetWindow<AddressableRuleSyncWindow>("Addressable Rule Sync");
|
||||
win.minSize = new Vector2(900, 520);
|
||||
win.Show();
|
||||
}
|
||||
|
||||
// ── GUI ───────────────────────────────────────────────────────────────
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
EnsureStyles();
|
||||
|
||||
if (AddressableAssetSettingsDefaultObject.Settings == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"Addressable Settings 未初始化。\n" +
|
||||
"请先执行 Window → Asset Management → Addressables → Groups → Create Addressables Settings。",
|
||||
MessageType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
DrawToolbar();
|
||||
DrawStats();
|
||||
DrawTable();
|
||||
DrawFooter();
|
||||
}
|
||||
|
||||
// ── 工具栏 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawToolbar()
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||||
{
|
||||
if (GUILayout.Button("扫描", EditorStyles.toolbarButton, GUILayout.Width(80)))
|
||||
Scan();
|
||||
|
||||
GUILayout.Space(8);
|
||||
_showOk = GUILayout.Toggle(_showOk, "显示正常项", EditorStyles.toolbarButton, GUILayout.Width(80));
|
||||
GUILayout.Space(8);
|
||||
|
||||
EditorGUILayout.LabelField("搜索:", GUILayout.Width(42));
|
||||
_searchFilter = EditorGUILayout.TextField(_searchFilter, EditorStyles.toolbarSearchField,
|
||||
GUILayout.Width(200));
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
GUI.enabled = _scanned && _reports.Any(r => !r.IsOk);
|
||||
if (GUILayout.Button("✦ 修复所有问题", EditorStyles.toolbarButton, GUILayout.Width(120)))
|
||||
FixAll();
|
||||
GUI.enabled = _scanned;
|
||||
if (GUILayout.Button("导出 CSV", EditorStyles.toolbarButton, GUILayout.Width(80)))
|
||||
ExportCsv();
|
||||
GUI.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 统计行 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawStats()
|
||||
{
|
||||
if (!_scanned) return;
|
||||
|
||||
int total = _reports.Count;
|
||||
int ok = _reports.Count(r => r.IsOk);
|
||||
int issues = total - ok;
|
||||
int wrongGrp = _reports.Count(r => !r.GroupOk);
|
||||
int misLabel = _reports.Count(r => r.MissingLabels.Length > 0);
|
||||
int extLabel = _reports.Count(r => r.ExtraLabels.Length > 0);
|
||||
|
||||
EditorGUILayout.Space(2);
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.Label($"共 {total} 条目", EditorStyles.miniLabel);
|
||||
GUILayout.Space(12);
|
||||
DrawColoredLabel($"✅ 正常 {ok}", ColOk);
|
||||
GUILayout.Space(12);
|
||||
DrawColoredLabel($"⚠ 问题 {issues}", issues > 0 ? ColWarn : ColOk);
|
||||
GUILayout.Space(20);
|
||||
GUILayout.Label($"分组错误 {wrongGrp} | 标签缺失 {misLabel} | 标签多余 {extLabel}",
|
||||
EditorStyles.miniLabel);
|
||||
GUILayout.FlexibleSpace();
|
||||
}
|
||||
EditorGUILayout.Space(2);
|
||||
}
|
||||
|
||||
// ── 主表格 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawTable()
|
||||
{
|
||||
// 表头
|
||||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||||
{
|
||||
GUILayout.Label("Address", _boldStyle, GUILayout.Width(200));
|
||||
GUILayout.Label("当前分组", _boldStyle, GUILayout.Width(130));
|
||||
GUILayout.Label("期望分组", _boldStyle, GUILayout.Width(130));
|
||||
GUILayout.Label("缺失标签", _boldStyle, GUILayout.Width(140));
|
||||
GUILayout.Label("多余标签", _boldStyle, GUILayout.Width(120));
|
||||
GUILayout.Label("状态", _boldStyle, GUILayout.Width(80));
|
||||
}
|
||||
|
||||
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos, GUILayout.ExpandHeight(true));
|
||||
|
||||
if (!_scanned)
|
||||
{
|
||||
EditorGUILayout.HelpBox("点击「扫描」按钮开始分析已注册的 Addressable 资产。", MessageType.Info);
|
||||
}
|
||||
else
|
||||
{
|
||||
var display = _reports
|
||||
.Where(r => _showOk || !r.IsOk)
|
||||
.Where(r => string.IsNullOrEmpty(_searchFilter)
|
||||
|| r.Address.IndexOf(_searchFilter, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
.ToList();
|
||||
|
||||
if (display.Count == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
_showOk ? "没有匹配搜索条件的条目。" : "✅ 所有资产均符合规范!",
|
||||
MessageType.Info);
|
||||
}
|
||||
|
||||
for (int i = 0; i < display.Count; i++)
|
||||
DrawRow(display[i], i);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
private void DrawRow(EntryReport r, int idx)
|
||||
{
|
||||
var bg = idx % 2 == 0 ? _rowEven : GUIStyle.none;
|
||||
using (new EditorGUILayout.HorizontalScope(bg, GUILayout.Height(20)))
|
||||
{
|
||||
// Address(点击可 Ping)
|
||||
if (GUILayout.Button(r.Address, EditorStyles.linkLabel, GUILayout.Width(200)))
|
||||
PingAsset(r.AssetPath);
|
||||
|
||||
// 当前分组
|
||||
var grpColor = r.GroupOk ? ColOk : ColError;
|
||||
DrawColoredLabel(r.CurrentGroup ?? "—", grpColor, GUILayout.Width(130));
|
||||
|
||||
// 期望分组
|
||||
var expGrpText = r.ExpectedGroup ?? "(规则未覆盖)";
|
||||
var expGrpColor = r.GroupOk ? ColOk : ColWarn;
|
||||
DrawColoredLabel(expGrpText, expGrpColor, GUILayout.Width(130));
|
||||
|
||||
// 缺失标签
|
||||
var missingText = r.MissingLabels.Length > 0 ? string.Join(", ", r.MissingLabels) : "—";
|
||||
DrawColoredLabel(missingText, r.MissingLabels.Length > 0 ? ColError : ColOk, GUILayout.Width(140));
|
||||
|
||||
// 多余标签
|
||||
var extraText = r.ExtraLabels.Length > 0 ? string.Join(", ", r.ExtraLabels) : "—";
|
||||
DrawColoredLabel(extraText, r.ExtraLabels.Length > 0 ? ColWarn : ColOk, GUILayout.Width(120));
|
||||
|
||||
// 状态 + 单条修复按钮
|
||||
if (r.IsOk)
|
||||
{
|
||||
DrawColoredLabel("✅ 正常", ColOk, GUILayout.Width(80));
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawColoredLabel("⚠ 需修复", ColWarn, GUILayout.Width(60));
|
||||
if (GUILayout.Button("修复", EditorStyles.miniButton, GUILayout.Width(40)))
|
||||
FixEntry(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 底栏 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawFooter()
|
||||
{
|
||||
EditorGUILayout.Space(4);
|
||||
EditorGUILayout.HelpBox(
|
||||
"规则来源:Docs/Standards/AddressablesLabelSpec.md §3 分组规则:AssetFolderSpec.md §8.1\n" +
|
||||
"「修复所有问题」仅修改已注册资产的分组/标签,不注册新资产(请用 Addressable Batch Tool)。",
|
||||
MessageType.None);
|
||||
}
|
||||
|
||||
// ── 扫描逻辑 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void Scan()
|
||||
{
|
||||
_reports.Clear();
|
||||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||||
if (settings == null) return;
|
||||
|
||||
// 收集所有全局标签供"多余标签"判断
|
||||
var allKnownLabels = new HashSet<string>(settings.GetLabels(), StringComparer.Ordinal);
|
||||
|
||||
foreach (var group in settings.groups)
|
||||
{
|
||||
if (group == null) continue;
|
||||
foreach (var entry in group.entries)
|
||||
{
|
||||
if (entry == null) continue;
|
||||
|
||||
var address = entry.address;
|
||||
var expectedGroup = AddressableRules.GetExpectedGroup(address);
|
||||
var expectedLbls = AddressableRules.GetExpectedLabels(address);
|
||||
var currentLbls = entry.labels.ToArray();
|
||||
|
||||
var missing = expectedLbls.Except(currentLbls, StringComparer.Ordinal).ToArray();
|
||||
var extra = currentLbls.Except(expectedLbls, StringComparer.Ordinal).ToArray();
|
||||
|
||||
_reports.Add(new EntryReport
|
||||
{
|
||||
Address = address,
|
||||
AssetPath = entry.AssetPath,
|
||||
CurrentGroup = group.name,
|
||||
ExpectedGroup = expectedGroup,
|
||||
CurrentLabels = currentLbls,
|
||||
ExpectedLabels = expectedLbls,
|
||||
MissingLabels = missing,
|
||||
ExtraLabels = extra,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 问题项排前面,正常项排后面;同类按 Address 字母序
|
||||
_reports = _reports
|
||||
.OrderBy(r => r.IsOk)
|
||||
.ThenBy(r => r.Address, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
_scanned = true;
|
||||
Repaint();
|
||||
Debug.Log($"[AddressableRuleSync] 扫描完成:{_reports.Count} 个条目," +
|
||||
$"{_reports.Count(r => !r.IsOk)} 个需要修复。");
|
||||
}
|
||||
|
||||
// ── 修复逻辑 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void FixAll()
|
||||
{
|
||||
var issues = _reports.Where(r => !r.IsOk).ToList();
|
||||
if (issues.Count == 0) return;
|
||||
|
||||
int fixedCount = 0;
|
||||
foreach (var r in issues)
|
||||
{
|
||||
if (FixEntry(r)) fixedCount++;
|
||||
}
|
||||
|
||||
SaveSettings();
|
||||
Scan(); // 修复后重新扫描以更新结果
|
||||
Debug.Log($"[AddressableRuleSync] 修复完成:共处理 {fixedCount} 个条目。");
|
||||
}
|
||||
|
||||
private bool FixEntry(EntryReport r)
|
||||
{
|
||||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||||
if (settings == null) return false;
|
||||
|
||||
var entry = FindEntry(settings, r.Address);
|
||||
if (entry == null)
|
||||
{
|
||||
Debug.LogWarning($"[AddressableRuleSync] 找不到条目:{r.Address}");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
|
||||
// 修复分组
|
||||
if (!r.GroupOk && r.ExpectedGroup != null)
|
||||
{
|
||||
var targetGroup = GetOrCreateGroup(settings, r.ExpectedGroup);
|
||||
if (targetGroup != null && entry.parentGroup != targetGroup)
|
||||
{
|
||||
settings.MoveEntry(entry, targetGroup, false, false);
|
||||
r.CurrentGroup = r.ExpectedGroup;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加缺失标签
|
||||
foreach (var lbl in r.MissingLabels)
|
||||
{
|
||||
EnsureLabelExists(settings, lbl);
|
||||
entry.SetLabel(lbl, true, true);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// 移除多余标签
|
||||
foreach (var lbl in r.ExtraLabels)
|
||||
{
|
||||
entry.SetLabel(lbl, false, true);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
// ── 导出 CSV ──────────────────────────────────────────────────────────
|
||||
|
||||
private void ExportCsv()
|
||||
{
|
||||
if (_reports.Count == 0) return;
|
||||
|
||||
var path = EditorUtility.SaveFilePanel(
|
||||
"导出 Addressable Rule 报告", "", "AddressableRuleReport.csv", "csv");
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("Address,CurrentGroup,ExpectedGroup,GroupOk,MissingLabels,ExtraLabels,Status");
|
||||
|
||||
foreach (var r in _reports)
|
||||
{
|
||||
var status = r.IsOk ? "OK" : "ISSUE";
|
||||
sb.AppendLine(
|
||||
$"\"{r.Address}\"," +
|
||||
$"\"{r.CurrentGroup}\"," +
|
||||
$"\"{r.ExpectedGroup ?? "(uncovered)"}\"," +
|
||||
$"{r.GroupOk}," +
|
||||
$"\"{string.Join(";", r.MissingLabels)}\"," +
|
||||
$"\"{string.Join(";", r.ExtraLabels)}\"," +
|
||||
$"{status}");
|
||||
}
|
||||
|
||||
File.WriteAllText(path, sb.ToString(), Encoding.UTF8);
|
||||
Debug.Log($"[AddressableRuleSync] CSV 报告已导出:{path}");
|
||||
}
|
||||
|
||||
// ── 辅助方法 ──────────────────────────────────────────────────────────
|
||||
|
||||
private static AddressableAssetEntry FindEntry(AddressableAssetSettings settings, string address)
|
||||
{
|
||||
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 static AddressableAssetGroup GetOrCreateGroup(AddressableAssetSettings settings, string groupName)
|
||||
{
|
||||
var existing = settings.groups.FirstOrDefault(g => g != null && g.name == groupName);
|
||||
if (existing != null) return existing;
|
||||
|
||||
var template = settings.GroupTemplateObjects.FirstOrDefault()
|
||||
as AddressableAssetGroupTemplate;
|
||||
var newGroup = settings.CreateGroup(groupName, false, false, true,
|
||||
template != null
|
||||
? new List<AddressableAssetGroupSchema>(template.SchemaObjects)
|
||||
: null);
|
||||
|
||||
if (newGroup != null)
|
||||
Debug.Log($"[AddressableRuleSync] 已自动创建分组:{groupName}");
|
||||
|
||||
return newGroup ?? settings.DefaultGroup;
|
||||
}
|
||||
|
||||
private static void EnsureLabelExists(AddressableAssetSettings settings, string label)
|
||||
{
|
||||
var labels = settings.GetLabels();
|
||||
if (!labels.Contains(label))
|
||||
{
|
||||
settings.AddLabel(label, true);
|
||||
Debug.Log($"[AddressableRuleSync] 已创建标签:{label}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void SaveSettings()
|
||||
{
|
||||
AssetDatabase.SaveAssets();
|
||||
AddressableAssetSettingsDefaultObject.Settings?.SetDirty(
|
||||
AddressableAssetSettings.ModificationEvent.EntryModified, null, true);
|
||||
}
|
||||
|
||||
private static void PingAsset(string assetPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assetPath)) return;
|
||||
var obj = AssetDatabase.LoadMainAssetAtPath(assetPath);
|
||||
if (obj != null) EditorGUIUtility.PingObject(obj);
|
||||
}
|
||||
|
||||
// ── 样式初始化 ────────────────────────────────────────────────────────
|
||||
|
||||
private void EnsureStyles()
|
||||
{
|
||||
if (_stylesReady) return;
|
||||
|
||||
_boldStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 11 };
|
||||
_okStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = ColOk } };
|
||||
_warnStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = ColWarn } };
|
||||
_errorStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = ColError } };
|
||||
|
||||
_rowEven = new GUIStyle();
|
||||
_rowEven.normal.background = MakeTexture(1, 1, ColRowEven);
|
||||
|
||||
_stylesReady = true;
|
||||
}
|
||||
|
||||
private void DrawColoredLabel(string text, Color color, params GUILayoutOption[] options)
|
||||
{
|
||||
var prev = GUI.color;
|
||||
GUI.color = color;
|
||||
GUILayout.Label(text, EditorStyles.miniLabel, options);
|
||||
GUI.color = prev;
|
||||
}
|
||||
|
||||
private static Texture2D MakeTexture(int width, int height, Color color)
|
||||
{
|
||||
var tex = new Texture2D(width, height);
|
||||
tex.SetPixel(0, 0, color);
|
||||
tex.Apply();
|
||||
return tex;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user