Files
5.24BuYu/Assets/Editor/AssetDependencyAnalyzer.cs
T
2026-07-29 22:59:26 +08:00

960 lines
32 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.Text;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine;
/// <summary>
/// 资源依赖分析器
/// 用于查找某个 Prefab / 资源 关联的所有依赖资源,并标记其在 Addressables 中的归属(Group、Address、Labels),
/// 供 AA 分包调查使用。
/// 菜单:Tools → 资源依赖分析器
/// </summary>
public class AssetDependencyAnalyzer : EditorWindow
{
// ============================ 日志开关 ============================
// 启用: Player Settings → Scripting Define Symbols 添加 ENABLE_EDITOR_LOG
// 关闭: 删除该符号。未定义时所有 Log/LogError 调用被编译器自动移除,零开销
[System.Diagnostics.Conditional("ENABLE_EDITOR_LOG")]
private static void Log(string msg) { Debug.Log(msg); }
[System.Diagnostics.Conditional("ENABLE_EDITOR_LOG")]
private static void LogError(string msg) { Debug.LogError(msg); }
#region 内部类
[Serializable]
public class DependencyInfo
{
public string assetPath;
public string guid;
public Type type;
public string typeName;
public bool isInAddressables;
public string aaAddress;
public string aaGroup;
public List<string> aaLabels = new List<string>();
public long fileSizeBytes;
public string fileSizeReadable;
}
#endregion
#region 字段
private Vector2 leftScroll;
private Vector2 rightScroll;
private Vector2 statsScroll;
// 输入:单个目标
private UnityEngine.Object targetAsset;
// 批量目标
private List<UnityEngine.Object> batchTargets = new List<UnityEngine.Object>();
private bool useBatchMode = false;
// 收集结果
private List<DependencyInfo> dependencies = new List<DependencyInfo>();
private Dictionary<string, DependencyInfo> dependencyMap = new Dictionary<string, DependencyInfo>();
// 反向查找(谁引用了这个资源)
private readonly Dictionary<string, List<string>> reverseRefMap = new Dictionary<string, List<string>>();
// 过滤
private enum FilterMode
{
All,
InAddressables,
NotInAddressables
}
private FilterMode filterMode = FilterMode.All;
private string typeFilter = "全部";
private string groupFilter = "全部";
private string labelFilter = "全部";
private string pathKeyword = "";
// 展示
private bool showStats = true;
private bool showFilter = true;
private bool showBatchList = true;
// 缓存
private AddressableAssetSettings aaSettings;
private List<string> allGroups = new List<string>();
private List<string> allLabels = new List<string>();
private List<string> allTypes = new List<string>();
private bool includeScripts = false;
private bool deepAnalyze = true;
#endregion
#region 菜单与初始化
[MenuItem("Tools/资源依赖分析器", false, 200)]
public static void OpenWindow()
{
var window = GetWindow<AssetDependencyAnalyzer>("资源依赖分析器");
window.minSize = new Vector2(1000, 600);
window.Show();
}
private void OnEnable()
{
RefreshAACache();
}
private void RefreshAACache()
{
aaSettings = AddressableAssetSettingsDefaultObject.Settings;
allGroups.Clear();
allLabels.Clear();
if (aaSettings != null)
{
foreach (var g in aaSettings.groups)
{
if (g != null) allGroups.Add(g.Name);
}
allLabels.AddRange(aaSettings.GetLabels());
}
}
#endregion
#region 主GUI
private void OnGUI()
{
DrawToolbar();
EditorGUILayout.Space(4);
DrawTargetSelector();
if (showFilter)
{
EditorGUILayout.Space(4);
DrawFilterBar();
}
if (showStats && dependencies.Count > 0)
{
EditorGUILayout.Space(4);
DrawStats();
}
EditorGUILayout.Space(4);
DrawDependencyList();
}
#endregion
#region 工具栏
private void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
if (GUILayout.Button("分析依赖", EditorStyles.toolbarButton, GUILayout.Width(80)))
{
AnalyzeAll();
}
if (GUILayout.Button("反向查找引用", EditorStyles.toolbarButton, GUILayout.Width(100)))
{
ReverseLookupSelected();
}
if (GUILayout.Button("导出报告", EditorStyles.toolbarButton, GUILayout.Width(80)))
{
ExportReport();
}
if (GUILayout.Button("复制AA地址", EditorStyles.toolbarButton, GUILayout.Width(90)))
{
CopySelectedAddresses();
}
if (GUILayout.Button("加入AA(选中)", EditorStyles.toolbarButton, GUILayout.Width(110)))
{
AddSelectedToAddressables();
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("刷新AA缓存", EditorStyles.toolbarButton, GUILayout.Width(90)))
{
RefreshAACache();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
showFilter = GUILayout.Toggle(showFilter, "过滤", EditorStyles.toolbarButton, GUILayout.Width(50));
showStats = GUILayout.Toggle(showStats, "统计", EditorStyles.toolbarButton, GUILayout.Width(50));
showBatchList = GUILayout.Toggle(showBatchList, "批量列表", EditorStyles.toolbarButton, GUILayout.Width(70));
useBatchMode = GUILayout.Toggle(useBatchMode, "批量模式", EditorStyles.toolbarButton, GUILayout.Width(70));
GUILayout.FlexibleSpace();
includeScripts = GUILayout.Toggle(includeScripts, "包含脚本", GUILayout.Width(80));
deepAnalyze = GUILayout.Toggle(deepAnalyze, "递归依赖", GUILayout.Width(80));
EditorGUILayout.EndHorizontal();
}
#endregion
#region 目标选择
private void DrawTargetSelector()
{
EditorGUILayout.BeginVertical("box");
if (!useBatchMode)
{
EditorGUILayout.LabelField("目标资源(Prefab / 贴图 / 音频 / 材质等,可拖拽)", EditorStyles.boldLabel);
var newObj = EditorGUILayout.ObjectField("目标", targetAsset, typeof(UnityEngine.Object), false);
if (newObj != targetAsset)
{
targetAsset = newObj;
}
EditorGUILayout.HelpBox(
"操作说明:\n" +
"1. 拖拽或选择目标资源(通常是 Prefab)。\n" +
"2. 点击「分析依赖」递归收集所有依赖资源。\n" +
"3. 通过过滤栏查看 AA 内 / 非 AA 资源,便于分包决策。\n" +
"4. 选中非 AA 资源后点击「加入AA」可直接添加到指定 Group。\n" +
"5. 「反向查找引用」会扫描所有 AA 条目,找出谁引用了当前选中资源。",
MessageType.Info);
}
else
{
EditorGUILayout.LabelField("批量目标列表(可拖拽多个资源到下方区域)", EditorStyles.boldLabel);
// 拖拽区域
Rect dropRect = GUILayoutUtility.GetRect(0, 80, GUILayout.ExpandWidth(true));
GUI.Box(dropRect, "拖拽资源到此处添加到批量列表");
HandleDragDrop(dropRect);
// 列表显示
if (showBatchList && batchTargets.Count > 0)
{
leftScroll = EditorGUILayout.BeginScrollView(leftScroll, GUILayout.Height(120));
for (int i = 0; i < batchTargets.Count; i++)
{
EditorGUILayout.BeginHorizontal();
var obj = EditorGUILayout.ObjectField(batchTargets[i], typeof(UnityEngine.Object), false);
if (obj != batchTargets[i]) batchTargets[i] = obj;
if (GUILayout.Button("×", GUILayout.Width(20)))
{
batchTargets.RemoveAt(i);
break;
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("清空列表", GUILayout.Width(80)))
{
batchTargets.Clear();
}
EditorGUILayout.LabelField($"共 {batchTargets.Count} 个目标", EditorStyles.miniLabel);
EditorGUILayout.EndHorizontal();
}
}
EditorGUILayout.EndVertical();
}
private void HandleDragDrop(Rect dropRect)
{
var evt = Event.current;
if (!dropRect.Contains(evt.mousePosition)) return;
if (evt.type == EventType.DragUpdated)
{
DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
evt.Use();
}
else if (evt.type == EventType.DragPerform)
{
DragAndDrop.AcceptDrag();
foreach (var path in DragAndDrop.paths)
{
var obj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
if (obj != null && !batchTargets.Contains(obj))
{
batchTargets.Add(obj);
}
}
evt.Use();
}
}
#endregion
#region 过滤栏
private void DrawFilterBar()
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.BeginHorizontal();
filterMode = (FilterMode)EditorGUILayout.EnumPopup("归属", filterMode, GUILayout.Width(180));
// 类型过滤
var typeOptions = new List<string> { "全部" };
typeOptions.AddRange(allTypes);
int typeIdx = Mathf.Max(0, typeOptions.IndexOf(typeFilter));
typeIdx = EditorGUILayout.Popup("类型", typeIdx, typeOptions.ToArray(), GUILayout.Width(180));
typeFilter = typeOptions[typeIdx];
// Group过滤
var groupOptions = new List<string> { "全部", "(非AA)" };
groupOptions.AddRange(allGroups);
int gIdx = Mathf.Max(0, groupOptions.IndexOf(groupFilter));
gIdx = EditorGUILayout.Popup("Group", gIdx, groupOptions.ToArray(), GUILayout.Width(180));
groupFilter = groupOptions[gIdx];
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
var labelOptions = new List<string> { "全部", "(无标签)" };
labelOptions.AddRange(allLabels);
int lIdx = Mathf.Max(0, labelOptions.IndexOf(labelFilter));
lIdx = EditorGUILayout.Popup("Label", lIdx, labelOptions.ToArray(), GUILayout.Width(180));
labelFilter = labelOptions[lIdx];
pathKeyword = EditorGUILayout.TextField("路径关键字", pathKeyword, GUILayout.Width(220));
if (GUILayout.Button("清空过滤", GUILayout.Width(80)))
{
filterMode = FilterMode.All;
typeFilter = "全部";
groupFilter = "全部";
labelFilter = "全部";
pathKeyword = "";
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
#endregion
#region 统计
private void DrawStats()
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("统计", EditorStyles.boldLabel);
int totalCount = dependencies.Count;
int aaCount = dependencies.Count(d => d.isInAddressables);
int nonAACount = totalCount - aaCount;
long totalSize = dependencies.Sum(d => d.fileSizeBytes);
EditorGUILayout.LabelField($"总依赖数: {totalCount} AA内: {aaCount} 非AA: {nonAACount} 总大小: {FormatSize(totalSize)}");
// 按类型统计
statsScroll = EditorGUILayout.BeginScrollView(statsScroll, GUILayout.Height(140));
var byType = dependencies.GroupBy(d => d.typeName).OrderByDescending(g => g.Count());
EditorGUILayout.LabelField("按类型统计:", EditorStyles.miniBoldLabel);
foreach (var grp in byType)
{
long s = grp.Sum(d => d.fileSizeBytes);
int aaInGroup = grp.Count(d => d.isInAddressables);
EditorGUILayout.LabelField($" {grp.Key}: {grp.Count()} 个 (AA内 {aaInGroup}) 大小 {FormatSize(s)}", EditorStyles.miniLabel);
}
// 按 Group 统计
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("按 AA Group 统计:", EditorStyles.miniBoldLabel);
var byGroup = dependencies.Where(d => d.isInAddressables).GroupBy(d => d.aaGroup).OrderByDescending(g => g.Count());
foreach (var grp in byGroup)
{
long s = grp.Sum(d => d.fileSizeBytes);
EditorGUILayout.LabelField($" {grp.Key}: {grp.Count()} 个 大小 {FormatSize(s)}", EditorStyles.miniLabel);
}
// 非 AA 资源按目录统计
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("非AA资源按目录统计:", EditorStyles.miniBoldLabel);
var nonAAByDir = dependencies
.Where(d => !d.isInAddressables)
.GroupBy(d => Path.GetDirectoryName(d.assetPath)?.Replace('\\', '/'))
.OrderByDescending(g => g.Count());
foreach (var grp in nonAAByDir)
{
long s = grp.Sum(d => d.fileSizeBytes);
EditorGUILayout.LabelField($" {grp.Key}: {grp.Count()} 个 大小 {FormatSize(s)}", EditorStyles.miniLabel);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
}
#endregion
#region 依赖列表
private void DrawDependencyList()
{
if (dependencies.Count == 0)
{
EditorGUILayout.HelpBox("尚未分析。请选择目标资源后点击「分析依赖」。", MessageType.Info);
return;
}
var filtered = ApplyFilter(dependencies);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"依赖列表 (过滤后: {filtered.Count} / {dependencies.Count})", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
if (GUILayout.Button("全选非AA", GUILayout.Width(80)))
{
SelectAllNonAA();
}
EditorGUILayout.EndHorizontal();
rightScroll = EditorGUILayout.BeginScrollView(rightScroll);
// 表头
EditorGUILayout.BeginHorizontal("box");
EditorGUILayout.LabelField("", GUILayout.Width(20));
EditorGUILayout.LabelField("资源路径", GUILayout.Width(300));
EditorGUILayout.LabelField("类型", GUILayout.Width(80));
EditorGUILayout.LabelField("大小", GUILayout.Width(70));
EditorGUILayout.LabelField("AA归属", GUILayout.Width(120));
EditorGUILayout.LabelField("Address", GUILayout.Width(150));
EditorGUILayout.LabelField("Labels", GUILayout.Width(150));
EditorGUILayout.EndHorizontal();
foreach (var info in filtered)
{
DrawDependencyRow(info);
}
EditorGUILayout.EndScrollView();
}
private List<DependencyInfo> GetFilteredCache()
{
return ApplyFilter(dependencies);
}
private void SelectAllNonAA()
{
var filtered = GetFilteredCache();
var nonAA = filtered.Where(d => !d.isInAddressables).Select(d => d.assetPath).ToList();
var objs = new List<UnityEngine.Object>();
foreach (var p in nonAA)
{
var o = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(p);
if (o != null) objs.Add(o);
}
Selection.objects = objs.ToArray();
Log($"[依赖分析器] 已选中 {objs.Count} 个非AA资源到 Project 视图");
}
private void DrawDependencyRow(DependencyInfo info)
{
EditorGUILayout.BeginHorizontal(GUIStyle.none);
// 复选框(仅展示,不维护勾选状态,使用 Selection.paths 作为统一选择机制)
bool selected = Selection.objects != null && Selection.objects.Any(o =>
o != null && AssetDatabase.GetAssetPath(o) == info.assetPath);
bool newSel = EditorGUILayout.Toggle(selected, GUILayout.Width(20));
if (newSel != selected)
{
var obj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(info.assetPath);
if (obj != null)
{
var list = Selection.objects != null ? Selection.objects.ToList() : new List<UnityEngine.Object>();
if (newSel && !list.Contains(obj)) list.Add(obj);
if (!newSel && list.Contains(obj)) list.Remove(obj);
Selection.objects = list.ToArray();
}
}
// 路径(可点击定位)
if (GUILayout.Button(info.assetPath, EditorStyles.label, GUILayout.Width(300)))
{
var obj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(info.assetPath);
if (obj != null)
{
EditorGUIUtility.PingObject(obj);
Selection.activeObject = obj;
}
}
EditorGUILayout.LabelField(info.typeName, GUILayout.Width(80));
EditorGUILayout.LabelField(info.fileSizeReadable, GUILayout.Width(70));
// AA 归属(彩色显示)
var oldColor = GUI.color;
GUI.color = info.isInAddressables ? new Color(0.6f, 1f, 0.6f) : new Color(1f, 0.6f, 0.6f);
EditorGUILayout.LabelField(info.isInAddressables ? info.aaGroup : "非AA", GUILayout.Width(120));
GUI.color = oldColor;
EditorGUILayout.LabelField(info.aaAddress ?? "-", GUILayout.Width(150));
EditorGUILayout.LabelField(info.aaLabels.Count > 0 ? string.Join(",", info.aaLabels) : "-", GUILayout.Width(150));
EditorGUILayout.EndHorizontal();
}
private List<DependencyInfo> ApplyFilter(List<DependencyInfo> source)
{
var result = source;
switch (filterMode)
{
case FilterMode.InAddressables:
result = result.Where(d => d.isInAddressables).ToList();
break;
case FilterMode.NotInAddressables:
result = result.Where(d => !d.isInAddressables).ToList();
break;
}
if (typeFilter != "全部")
result = result.Where(d => d.typeName == typeFilter).ToList();
if (groupFilter == "(非AA)")
result = result.Where(d => !d.isInAddressables).ToList();
else if (groupFilter != "全部")
result = result.Where(d => d.isInAddressables && d.aaGroup == groupFilter).ToList();
if (labelFilter == "(无标签)")
result = result.Where(d => d.isInAddressables && d.aaLabels.Count == 0).ToList();
else if (labelFilter != "全部")
result = result.Where(d => d.isInAddressables && d.aaLabels.Contains(labelFilter)).ToList();
if (!string.IsNullOrEmpty(pathKeyword))
result = result.Where(d => d.assetPath.IndexOf(pathKeyword, StringComparison.OrdinalIgnoreCase) >= 0).ToList();
return result;
}
#endregion
#region 分析逻辑
private void AnalyzeAll()
{
dependencies.Clear();
dependencyMap.Clear();
allTypes.Clear();
RefreshAACache();
var targetPaths = new HashSet<string>();
if (useBatchMode)
{
foreach (var obj in batchTargets)
{
if (obj == null) continue;
string p = AssetDatabase.GetAssetPath(obj);
if (!string.IsNullOrEmpty(p)) targetPaths.Add(p);
}
}
else
{
if (targetAsset != null)
{
string p = AssetDatabase.GetAssetPath(targetAsset);
if (!string.IsNullOrEmpty(p)) targetPaths.Add(p);
}
}
if (targetPaths.Count == 0)
{
EditorUtility.DisplayDialog("提示", "请先选择目标资源。", "确定");
return;
}
try
{
EditorUtility.DisplayProgressBar("分析依赖", "正在收集依赖资源...", 0f);
int idx = 0;
foreach (string path in targetPaths)
{
EditorUtility.DisplayProgressBar("分析依赖", $"处理 {path}", (float)idx / targetPaths.Count);
AnalyzeSingle(path);
idx++;
}
// 收集类型列表
allTypes = dependencies.Select(d => d.typeName).Distinct().OrderBy(t => t).ToList();
EditorUtility.ClearProgressBar();
Log($"[依赖分析器] 分析完成,共 {dependencies.Count} 个依赖资源");
}
catch (Exception e)
{
EditorUtility.ClearProgressBar();
LogError($"[依赖分析器] 分析失败: {e}");
}
}
private void AnalyzeSingle(string assetPath)
{
if (string.IsNullOrEmpty(assetPath)) return;
// 自身也作为一项
AddDependencyInfo(assetPath);
if (!deepAnalyze) return;
string[] deps = AssetDatabase.GetDependencies(assetPath, true);
foreach (var dep in deps)
{
AddDependencyInfo(dep);
}
}
private void AddDependencyInfo(string assetPath)
{
if (dependencyMap.ContainsKey(assetPath)) return;
if (!includeScripts && assetPath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
return;
// 过滤 Editor 仅资源
if (assetPath.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase))
return;
var info = new DependencyInfo
{
assetPath = assetPath,
guid = AssetDatabase.AssetPathToGUID(assetPath)
};
var mainAsset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath);
if (mainAsset != null)
{
info.type = mainAsset.GetType();
info.typeName = info.type.Name;
}
else
{
info.typeName = Path.GetExtension(assetPath).TrimStart('.');
}
// 文件大小
try
{
string fullPath = Path.Combine(Directory.GetParent(Application.dataPath).FullName, assetPath);
if (File.Exists(fullPath))
{
var fi = new FileInfo(fullPath);
info.fileSizeBytes = fi.Length;
info.fileSizeReadable = FormatSize(info.fileSizeBytes);
}
else
{
info.fileSizeBytes = 0;
info.fileSizeReadable = "-";
}
}
catch
{
info.fileSizeBytes = 0;
info.fileSizeReadable = "-";
}
// AA 信息
if (aaSettings != null && !string.IsNullOrEmpty(info.guid))
{
var entry = aaSettings.FindAssetEntry(info.guid);
if (entry != null)
{
info.isInAddressables = true;
info.aaAddress = entry.address;
info.aaGroup = entry.parentGroup != null ? entry.parentGroup.Name : "(未知)";
info.aaLabels = entry.labels.ToList();
}
}
dependencies.Add(info);
dependencyMap[assetPath] = info;
}
#endregion
#region 反向查找
private void ReverseLookupSelected()
{
if (aaSettings == null)
{
EditorUtility.DisplayDialog("提示", "Addressables 未初始化。", "确定");
return;
}
var selected = Selection.objects;
if (selected == null || selected.Length == 0)
{
EditorUtility.DisplayDialog("提示", "请先在 Project 视图中选择目标资源。", "确定");
return;
}
reverseRefMap.Clear();
var targetGuids = new HashSet<string>();
foreach (var obj in selected)
{
string p = AssetDatabase.GetAssetPath(obj);
if (!string.IsNullOrEmpty(p))
{
string g = AssetDatabase.AssetPathToGUID(p);
if (!string.IsNullOrEmpty(g)) targetGuids.Add(g);
}
}
try
{
EditorUtility.DisplayProgressBar("反向查找", "正在遍历 AA 条目...", 0f);
var allEntries = aaSettings.groups.SelectMany(g => g.entries).ToList();
int idx = 0;
int matched = 0;
foreach (var entry in allEntries)
{
idx++;
if (idx % 50 == 0)
EditorUtility.DisplayProgressBar("反向查找", $"扫描 {idx}/{allEntries.Count}", (float)idx / allEntries.Count);
string entryPath = AssetDatabase.GUIDToAssetPath(entry.guid);
if (string.IsNullOrEmpty(entryPath)) continue;
string[] deps = AssetDatabase.GetDependencies(entryPath, true);
foreach (var dep in deps)
{
string depGuid = AssetDatabase.AssetPathToGUID(dep);
if (targetGuids.Contains(depGuid))
{
if (!reverseRefMap.ContainsKey(dep))
reverseRefMap[dep] = new List<string>();
if (!reverseRefMap[dep].Contains(entryPath))
reverseRefMap[dep].Add(entryPath);
matched++;
}
}
}
EditorUtility.ClearProgressBar();
var sb = new StringBuilder();
sb.AppendLine($"反向查找完成,命中 {matched} 处引用。");
sb.AppendLine();
foreach (var kv in reverseRefMap)
{
sb.AppendLine($"【目标】{kv.Key}");
foreach (var refPath in kv.Value)
{
string refGuid = AssetDatabase.AssetPathToGUID(refPath);
var refEntry = aaSettings.FindAssetEntry(refGuid);
string aaInfo = refEntry != null
? $"[AA] {refEntry.parentGroup.Name} / {refEntry.address}"
: "[非AA]";
sb.AppendLine($" ← {refPath} {aaInfo}");
}
sb.AppendLine();
}
string savePath = EditorUtility.SaveFilePanel("保存反向查找报告", Application.dataPath, "ReverseLookupReport", "txt");
if (!string.IsNullOrEmpty(savePath))
{
File.WriteAllText(savePath, sb.ToString(), Encoding.UTF8);
EditorUtility.DisplayDialog("完成", $"报告已保存到:\n{savePath}", "确定");
}
else
{
Log(sb.ToString());
}
}
catch (Exception e)
{
EditorUtility.ClearProgressBar();
LogError($"[依赖分析器] 反向查找失败: {e}");
}
}
#endregion
#region 导出报告
private void ExportReport()
{
if (dependencies.Count == 0)
{
EditorUtility.DisplayDialog("提示", "请先分析依赖。", "确定");
return;
}
string savePath = EditorUtility.SaveFilePanel("导出依赖报告", Application.dataPath, "DependencyReport", "csv");
if (string.IsNullOrEmpty(savePath)) return;
var sb = new StringBuilder();
sb.AppendLine("AssetPath,GUID,Type,Size,IsInAA,AAGroup,AAAddress,AALabels");
foreach (var info in dependencies)
{
sb.AppendLine(string.Join(",",
EscapeCsv(info.assetPath),
info.guid,
info.typeName,
info.fileSizeBytes,
info.isInAddressables,
EscapeCsv(info.aaGroup ?? ""),
EscapeCsv(info.aaAddress ?? ""),
EscapeCsv(string.Join(";", info.aaLabels))));
}
File.WriteAllText(savePath, sb.ToString(), Encoding.UTF8);
EditorUtility.DisplayDialog("完成", $"报告已导出到:\n{savePath}", "确定");
Log($"[依赖分析器] 报告已导出: {savePath}");
}
private string EscapeCsv(string s)
{
if (string.IsNullOrEmpty(s)) return "";
if (s.Contains(",") || s.Contains("\"") || s.Contains("\n"))
{
return "\"" + s.Replace("\"", "\"\"") + "\"";
}
return s;
}
#endregion
#region 复制地址
private void CopySelectedAddresses()
{
var filtered = GetFilteredCache();
var selected = filtered.Where(d =>
Selection.objects != null &&
Selection.objects.Any(o => o != null && AssetDatabase.GetAssetPath(o) == d.assetPath))
.ToList();
if (selected.Count == 0)
{
EditorUtility.DisplayDialog("提示", "请先在下方列表勾选资源。", "确定");
return;
}
var sb = new StringBuilder();
foreach (var info in selected)
{
sb.AppendLine(info.aaAddress ?? info.assetPath);
}
GUIUtility.systemCopyBuffer = sb.ToString();
Log($"[依赖分析器] 已复制 {selected.Count} 条地址到剪贴板");
}
#endregion
#region 加入AA
private void AddSelectedToAddressables()
{
if (aaSettings == null)
{
EditorUtility.DisplayDialog("错误", "Addressables 未初始化。", "确定");
return;
}
var filtered = GetFilteredCache();
var selected = filtered.Where(d =>
Selection.objects != null &&
Selection.objects.Any(o => o != null && AssetDatabase.GetAssetPath(o) == d.assetPath))
.ToList();
if (selected.Count == 0)
{
EditorUtility.DisplayDialog("提示", "请先在下方列表勾选资源。", "确定");
return;
}
// 选择 Group
var groupNames = aaSettings.groups.Where(g => g != null).Select(g => g.Name).ToArray();
int choice = EditorUtility.DisplayDialogComplex(
"选择目标 Group",
$"将 {selected.Count} 个资源添加到哪个 Group?",
groupNames.Length > 0 ? groupNames[0] : "Default",
groupNames.Length > 1 ? groupNames[1] : "Cancel",
groupNames.Length > 2 ? "其他..." : "Cancel");
if (choice == 2)
{
// 弹出菜单选择
var menu = new GenericMenu();
foreach (var gn in groupNames)
{
var captured = gn;
menu.AddItem(new GUIContent(gn), false, () => DoAddToGroup(selected, captured));
}
menu.ShowAsContext();
return;
}
else if (choice >= 0 && choice < groupNames.Length)
{
DoAddToGroup(selected, groupNames[choice]);
}
}
private void DoAddToGroup(List<DependencyInfo> list, string groupName)
{
var group = aaSettings.groups.FirstOrDefault(g => g != null && g.Name == groupName);
if (group == null)
{
EditorUtility.DisplayDialog("错误", $"找不到 Group: {groupName}", "确定");
return;
}
int count = 0;
var errors = new List<string>();
foreach (var info in list)
{
if (string.IsNullOrEmpty(info.guid))
{
errors.Add($"{info.assetPath}: GUID 为空");
continue;
}
var entry = aaSettings.FindAssetEntry(info.guid);
if (entry == null)
entry = aaSettings.CreateOrMoveEntry(info.guid, group);
if (entry == null)
{
errors.Add($"{info.assetPath}: 创建条目失败");
continue;
}
// 默认 address 用文件名
if (string.IsNullOrEmpty(entry.address) || entry.address == info.guid)
entry.address = Path.GetFileNameWithoutExtension(info.assetPath);
count++;
}
aaSettings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryMoved, null, true);
AssetDatabase.SaveAssets();
// 重新分析刷新
AnalyzeAll();
string msg = $"成功: {count} 个";
if (errors.Count > 0) msg += $"\n失败 {errors.Count} 个:\n" + string.Join("\n", errors);
EditorUtility.DisplayDialog("加入AA完成", msg, "确定");
}
#endregion
#region 工具
private static string FormatSize(long bytes)
{
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return (bytes / 1024.0).ToString("F1") + " KB";
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024.0 * 1024)).ToString("F2") + " MB";
return (bytes / (1024.0 * 1024 * 1024)).ToString("F2") + " GB";
}
#endregion
}