using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEngine;
namespace BaseGames.Editor
{
///
/// Addressable Key 引用关系图窗口(架构 13_AssetPoolModule §11)。
/// 菜单:BaseGames/Tools/Asset Reference Graph
///
/// 功能:
/// - 扫描所有 .cs 文件中对 AddressKeys.X 的引用
/// - 列出每个 Key:声明位置、引用文件列表、是否存在于 Addressables
/// - 孤儿 Key(有声明无引用)标红显示
/// - 无效 Key(有引用但不存在于 Addressables)标橙显示
/// - 一键导出 CSV
///
public class AddressReferenceGraphWindow : EditorWindow
{
// ── State ──────────────────────────────────────────────────────────
private List _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/Addressables/Asset Reference Graph")]
public static void OpenWindow()
{
var win = GetWindow("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();
// 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();
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()
};
}
// 2. 检查 Addressables(直接内联,不依赖 Verification 程序集)
var registeredAddressValues = GetRegisteredAddressableAddresses();
foreach (var kv in keyDict)
kv.Value.ExistsInAddressables = registeredAddressValues.Contains(kv.Value.Value);
// 3. 扫描 _Game/ 下所有 .cs 文件中对 AddressKeys 的引用
string gameDir = Path.Combine(Application.dataPath, "_Game");
if (!Directory.Exists(gameDir))
{
Debug.LogWarning($"[AddressReferenceGraph] 未找到游戏目录:{gameDir}");
foreach (var kv in keyDict) _entries.Add(kv.Value);
return;
}
var csFiles = Directory.GetFiles(gameDir, "*.cs", SearchOption.AllDirectories);
foreach (var file in csFiles)
{
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}");
}
// ── Addressables 辅助(独立实现,不依赖 Verification 程序集)─────────
private static HashSet GetRegisteredAddressableAddresses()
{
var addresses = new HashSet(StringComparer.Ordinal);
var settings = AddressableAssetSettingsDefaultObject.Settings;
if (settings == null) return addresses;
foreach (var group in settings.groups)
{
if (group == null) continue;
foreach (var entry in group.entries)
if (entry != null) addresses.Add(entry.address);
}
return addresses;
}
// ── Ping Addressable ──────────────────────────────────────────────
private static void PingAddressableAsset(string address)
{
#if UNITY_EDITOR
var guids = AssetDatabase.FindAssets($"\"{address}\"");
if (guids.Length > 0)
{
var obj = AssetDatabase.LoadAssetAtPath(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 ReferencedInFiles;
public int ReferenceCount => ReferencedInFiles?.Count ?? 0;
}
}
}