多轮审查和修复
This commit is contained in:
288
Assets/Scripts/Editor/AddressReferenceGraphWindow.cs
Normal file
288
Assets/Scripts/Editor/AddressReferenceGraphWindow.cs
Normal file
@@ -0,0 +1,288 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Addressable Key 引用关系图窗口(架构 13_AssetPoolModule §11)。
|
||||
/// 菜单:BaseGames/Tools/Asset Reference Graph
|
||||
///
|
||||
/// 功能:
|
||||
/// - 扫描所有 .cs 文件中对 AddressKeys.X 的引用
|
||||
/// - 列出每个 Key:声明位置、引用文件列表、是否存在于 Addressables
|
||||
/// - 孤儿 Key(有声明无引用)标红显示
|
||||
/// - 无效 Key(有引用但不存在于 Addressables)标橙显示
|
||||
/// - 一键导出 CSV
|
||||
/// </summary>
|
||||
public class AddressReferenceGraphWindow : EditorWindow
|
||||
{
|
||||
// ── State ──────────────────────────────────────────────────────────
|
||||
private List<KeyEntry> _entries;
|
||||
private Vector2 _scrollPos;
|
||||
private string _searchFilter = "";
|
||||
private bool _showOrphansOnly;
|
||||
private bool _showMissingOnly;
|
||||
|
||||
// ── Colors ─────────────────────────────────────────────────────────
|
||||
private static readonly Color ColOrphan = new Color(0.90f, 0.15f, 0.15f, 0.80f); // 孤儿 Key(无引用)
|
||||
private static readonly Color ColMissing = new Color(0.95f, 0.55f, 0.10f, 0.80f); // 无效 Key(不在 Addressables)
|
||||
private static readonly Color ColOk = new Color(0.20f, 0.75f, 0.30f, 0.80f); // 正常
|
||||
|
||||
[MenuItem("BaseGames/Tools/Asset Reference Graph")]
|
||||
public static void OpenWindow()
|
||||
{
|
||||
var win = GetWindow<AddressReferenceGraphWindow>("Asset Reference Graph");
|
||||
win.minSize = new Vector2(900, 500);
|
||||
win.Show();
|
||||
}
|
||||
|
||||
// ── GUI ────────────────────────────────────────────────────────────
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
DrawToolbar();
|
||||
|
||||
if (_entries == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox("点击上方「扫描」按钮分析 AddressKeys 引用关系。", MessageType.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
DrawFilterRow();
|
||||
DrawResults();
|
||||
}
|
||||
|
||||
// ── Toolbar ───────────────────────────────────────────────────────
|
||||
|
||||
private void DrawToolbar()
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
|
||||
|
||||
if (GUILayout.Button("扫描", EditorStyles.toolbarButton, GUILayout.Width(60)))
|
||||
RunScan();
|
||||
|
||||
if (_entries != null && GUILayout.Button("导出 CSV", EditorStyles.toolbarButton, GUILayout.Width(70)))
|
||||
ExportCsv();
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
if (_entries != null)
|
||||
{
|
||||
int orphans = _entries.Count(e => e.ReferenceCount == 0);
|
||||
int missing = _entries.Count(e => !e.ExistsInAddressables);
|
||||
EditorGUILayout.LabelField(
|
||||
$"共 {_entries.Count} 个 Key | 孤儿:{orphans} | 未在 Addressables:{missing}",
|
||||
EditorStyles.toolbarButton);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
// ── 过滤行 ────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawFilterRow()
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField("搜索:", GUILayout.Width(40));
|
||||
_searchFilter = EditorGUILayout.TextField(_searchFilter, GUILayout.ExpandWidth(true));
|
||||
_showOrphansOnly = EditorGUILayout.ToggleLeft("仅显示孤儿", _showOrphansOnly, GUILayout.Width(90));
|
||||
_showMissingOnly = EditorGUILayout.ToggleLeft("仅显示缺失", _showMissingOnly, GUILayout.Width(90));
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
// ── 结果列表 ──────────────────────────────────────────────────────
|
||||
|
||||
private void DrawResults()
|
||||
{
|
||||
var filtered = _entries.AsEnumerable();
|
||||
|
||||
if (_showOrphansOnly)
|
||||
filtered = filtered.Where(e => e.ReferenceCount == 0);
|
||||
if (_showMissingOnly)
|
||||
filtered = filtered.Where(e => !e.ExistsInAddressables);
|
||||
if (!string.IsNullOrEmpty(_searchFilter))
|
||||
filtered = filtered.Where(e =>
|
||||
e.FieldName.IndexOf(_searchFilter, System.StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
|
||||
var list = filtered.ToList();
|
||||
|
||||
// 表头
|
||||
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
|
||||
EditorGUILayout.LabelField("状态", GUILayout.Width(50));
|
||||
EditorGUILayout.LabelField("Key 名称", GUILayout.Width(280));
|
||||
EditorGUILayout.LabelField("地址值", GUILayout.Width(300));
|
||||
EditorGUILayout.LabelField("引用数", GUILayout.Width(60));
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
|
||||
|
||||
foreach (var entry in list)
|
||||
{
|
||||
bool isOrphan = entry.ReferenceCount == 0;
|
||||
bool isMissing = !entry.ExistsInAddressables;
|
||||
|
||||
Color statusColor = isOrphan ? ColOrphan : (isMissing ? ColMissing : ColOk);
|
||||
string statusIcon = isOrphan ? "⊘" : (isMissing ? "⚠" : "✓");
|
||||
|
||||
var prevBg = GUI.backgroundColor;
|
||||
GUI.backgroundColor = statusColor * 0.6f;
|
||||
EditorGUILayout.BeginHorizontal("box");
|
||||
GUI.backgroundColor = prevBg;
|
||||
|
||||
EditorGUILayout.LabelField(statusIcon, GUILayout.Width(50));
|
||||
EditorGUILayout.LabelField(entry.FieldName, GUILayout.Width(280));
|
||||
|
||||
// 地址值可点击 → Ping Addressable asset
|
||||
if (GUILayout.Button(entry.Value,
|
||||
isOrphan ? EditorStyles.label : EditorStyles.miniButtonMid,
|
||||
GUILayout.Width(300)))
|
||||
{
|
||||
PingAddressableAsset(entry.Value);
|
||||
}
|
||||
|
||||
EditorGUILayout.LabelField(
|
||||
$"{entry.ReferenceCount}",
|
||||
GUILayout.Width(60));
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
// 展开:显示引用文件列表
|
||||
if (entry.ReferenceCount > 0 && entry.ReferencedInFiles != null)
|
||||
{
|
||||
foreach (var file in entry.ReferencedInFiles)
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Space(60);
|
||||
EditorGUILayout.LabelField($" ↳ {file}", EditorStyles.miniLabel);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
// ── 扫描逻辑 ──────────────────────────────────────────────────────
|
||||
|
||||
private void RunScan()
|
||||
{
|
||||
_entries = new List<KeyEntry>();
|
||||
|
||||
// 1. 收集所有 AddressKeys 常量
|
||||
var keyFields = typeof(BaseGames.Core.Assets.AddressKeys)
|
||||
.GetFields(System.Reflection.BindingFlags.Public
|
||||
| System.Reflection.BindingFlags.Static
|
||||
| System.Reflection.BindingFlags.FlattenHierarchy)
|
||||
.Where(f => f.IsLiteral && !f.IsInitOnly && f.FieldType == typeof(string));
|
||||
|
||||
var keyDict = new Dictionary<string, KeyEntry>();
|
||||
foreach (var f in keyFields)
|
||||
{
|
||||
var value = (string)f.GetRawConstantValue();
|
||||
keyDict[f.Name] = new KeyEntry
|
||||
{
|
||||
FieldName = f.Name,
|
||||
Value = value,
|
||||
ExistsInAddressables = false,
|
||||
ReferencedInFiles = new List<string>()
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 检查 Addressables
|
||||
var registeredAddresses = AddressKeyValidator.RunValidation()
|
||||
.Where(r => r.ExistsInAddressables)
|
||||
.Select(r => r.FieldName)
|
||||
.ToHashSet();
|
||||
|
||||
foreach (var kv in keyDict)
|
||||
kv.Value.ExistsInAddressables = registeredAddresses.Contains(kv.Key);
|
||||
|
||||
// 3. 扫描 .cs 文件引用
|
||||
var csFiles = Directory.GetFiles(
|
||||
Path.Combine(Application.dataPath, "Scripts"),
|
||||
"*.cs",
|
||||
SearchOption.AllDirectories);
|
||||
|
||||
foreach (var file in csFiles)
|
||||
{
|
||||
string content;
|
||||
try { content = File.ReadAllText(file); }
|
||||
catch { continue; }
|
||||
|
||||
foreach (var kv in keyDict)
|
||||
{
|
||||
// 匹配 AddressKeys.FieldName(单词边界,避免前缀误匹配)
|
||||
if (Regex.IsMatch(content, $@"\bAddressKeys\.{Regex.Escape(kv.Key)}\b"))
|
||||
{
|
||||
string relativePath = "Assets" + file.Substring(Application.dataPath.Length).Replace('\\', '/');
|
||||
kv.Value.ReferencedInFiles.Add(relativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var kv in keyDict)
|
||||
_entries.Add(kv.Value);
|
||||
|
||||
_entries.Sort((a, b) =>
|
||||
{
|
||||
// 孤儿排最前,其次缺失,最后正常
|
||||
int aScore = a.ReferenceCount == 0 ? 0 : (!a.ExistsInAddressables ? 1 : 2);
|
||||
int bScore = b.ReferenceCount == 0 ? 0 : (!b.ExistsInAddressables ? 1 : 2);
|
||||
return aScore != bScore ? aScore.CompareTo(bScore) : string.Compare(a.FieldName, b.FieldName);
|
||||
});
|
||||
|
||||
Debug.Log($"[AddressReferenceGraph] 扫描完成:{_entries.Count} 个 Key," +
|
||||
$"{_entries.Count(e => e.ReferenceCount == 0)} 孤儿," +
|
||||
$"{_entries.Count(e => !e.ExistsInAddressables)} 未在 Addressables。");
|
||||
}
|
||||
|
||||
// ── CSV 导出 ──────────────────────────────────────────────────────
|
||||
|
||||
private void ExportCsv()
|
||||
{
|
||||
string path = EditorUtility.SaveFilePanel("导出 CSV", "", "AddressKeyReport", "csv");
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
|
||||
using var writer = new StreamWriter(path, false, System.Text.Encoding.UTF8);
|
||||
writer.WriteLine("FieldName,Value,ExistsInAddressables,ReferenceCount,ReferencedFiles");
|
||||
foreach (var e in _entries)
|
||||
{
|
||||
string files = e.ReferencedInFiles != null
|
||||
? string.Join(" | ", e.ReferencedInFiles)
|
||||
: "";
|
||||
writer.WriteLine($"{e.FieldName},{e.Value},{e.ExistsInAddressables},{e.ReferenceCount},{files}");
|
||||
}
|
||||
|
||||
Debug.Log($"[AddressReferenceGraph] CSV 已导出:{path}");
|
||||
}
|
||||
|
||||
// ── Ping Addressable ──────────────────────────────────────────────
|
||||
|
||||
private static void PingAddressableAsset(string address)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
var guids = AssetDatabase.FindAssets($"\"{address}\"");
|
||||
if (guids.Length > 0)
|
||||
{
|
||||
var obj = AssetDatabase.LoadAssetAtPath<Object>(AssetDatabase.GUIDToAssetPath(guids[0]));
|
||||
if (obj != null) EditorGUIUtility.PingObject(obj);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// ── Data ──────────────────────────────────────────────────────────
|
||||
|
||||
private class KeyEntry
|
||||
{
|
||||
public string FieldName;
|
||||
public string Value;
|
||||
public bool ExistsInAddressables;
|
||||
public List<string> ReferencedInFiles;
|
||||
public int ReferenceCount => ReferencedInFiles?.Count ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user