509 lines
22 KiB
C#
509 lines
22 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using UnityEditor;
|
||
using UnityEngine;
|
||
using UnityEngine.UIElements;
|
||
using BaseGames.Core;
|
||
using BaseGames.Dialogue;
|
||
using BaseGames.Quest;
|
||
using BaseGames.EventChain;
|
||
using BaseGames.Editor;
|
||
|
||
namespace BaseGames.Editor.Modules
|
||
{
|
||
/// <summary>
|
||
/// DataHub 标志审计模块 —— 扫描项目所有 WorldStateFlag 引用,
|
||
/// 检测孤立标志(已注册但从未使用)和未注册标志(已使用但未在注册表定义)。
|
||
/// </summary>
|
||
public class FlagAuditModule : IDataModule, IDataModuleOrdered
|
||
{
|
||
public string ModuleId => "flagaudit";
|
||
public string DisplayName => "标志审计";
|
||
public string IconName => "d_FilterByLabel";
|
||
public int DisplayOrder => 130;
|
||
|
||
// ── 数据 ─────────────────────────────────────────────────────────────
|
||
|
||
private readonly List<FlagRecord> _records = new();
|
||
private FlagRecord _selected;
|
||
private bool _hasScanned;
|
||
|
||
private class FlagRecord
|
||
{
|
||
public string id;
|
||
public string description;
|
||
public string group;
|
||
public bool isRegistered;
|
||
public readonly List<(string label, UnityEngine.Object asset)> setLocations = new();
|
||
public readonly List<(string label, UnityEngine.Object asset)> readLocations = new();
|
||
|
||
public bool IsOrphan => isRegistered && TotalUsages == 0;
|
||
public bool IsUnregistered => !isRegistered;
|
||
public int TotalUsages => setLocations.Count + readLocations.Count;
|
||
}
|
||
|
||
// ── UI 引用 ───────────────────────────────────────────────────────────
|
||
|
||
private VisualElement _listItems;
|
||
private Label _summaryLabel;
|
||
private VisualElement _detailRoot;
|
||
private bool _filterOrphan, _filterUnregistered;
|
||
|
||
// ── IDataModule ───────────────────────────────────────────────────────
|
||
|
||
public void Initialize() { }
|
||
|
||
public void BuildListPane(VisualElement container, Action<UnityEngine.Object> onSelected)
|
||
{
|
||
// 扫描按钮
|
||
var scanBtn = new Button(RunScan) { text = "🔍 扫描标志使用情况" };
|
||
scanBtn.style.marginTop = 8;
|
||
scanBtn.style.marginLeft = 8;
|
||
scanBtn.style.marginRight = 8;
|
||
scanBtn.style.marginBottom = 4;
|
||
container.Add(scanBtn);
|
||
|
||
// 统计行
|
||
_summaryLabel = new Label("尚未扫描,点击上方按钮开始。");
|
||
_summaryLabel.style.fontSize = 10;
|
||
_summaryLabel.style.opacity = 0.6f;
|
||
_summaryLabel.style.paddingLeft = 10;
|
||
_summaryLabel.style.marginBottom = 4;
|
||
container.Add(_summaryLabel);
|
||
|
||
// 过滤标签行
|
||
var filterRow = new VisualElement();
|
||
filterRow.style.flexDirection = FlexDirection.Row;
|
||
filterRow.style.flexWrap = Wrap.Wrap;
|
||
filterRow.style.paddingLeft = 6;
|
||
filterRow.style.paddingRight = 6;
|
||
filterRow.style.paddingBottom = 3;
|
||
container.Add(filterRow);
|
||
|
||
filterRow.Add(DataHubEditorKit.MakeFilterChip("仅孤立", v => { _filterOrphan = v; RebuildList(); }));
|
||
filterRow.Add(DataHubEditorKit.MakeFilterChip("仅未注册", v => { _filterUnregistered = v; RebuildList(); }));
|
||
|
||
// 列表 ScrollView
|
||
var scroll = new ScrollView();
|
||
scroll.style.flexGrow = 1;
|
||
container.Add(scroll);
|
||
|
||
_listItems = new VisualElement();
|
||
scroll.Add(_listItems);
|
||
}
|
||
|
||
public void BuildDetailPane(VisualElement container, UnityEngine.Object selected)
|
||
{
|
||
_detailRoot = container;
|
||
RebuildDetail();
|
||
}
|
||
|
||
public void OnActivated() { }
|
||
|
||
// ── 扫描 ──────────────────────────────────────────────────────────────
|
||
|
||
private void RunScan()
|
||
{
|
||
_records.Clear();
|
||
_hasScanned = true;
|
||
|
||
var byId = new Dictionary<string, FlagRecord>(StringComparer.Ordinal);
|
||
FlagRecord GetOrCreate(string id)
|
||
{
|
||
if (!byId.TryGetValue(id, out var r))
|
||
{
|
||
r = new FlagRecord { id = id };
|
||
byId[id] = r;
|
||
_records.Add(r);
|
||
}
|
||
return r;
|
||
}
|
||
|
||
// 1. 从 WorldFlagRegistrySO 导入注册表
|
||
var registry = WorldFlagRegistrySO.EditorInstance;
|
||
if (registry?.flags != null)
|
||
foreach (var entry in registry.flags)
|
||
{
|
||
if (string.IsNullOrEmpty(entry.id)) continue;
|
||
var r = GetOrCreate(entry.id);
|
||
r.isRegistered = true;
|
||
r.description = entry.description;
|
||
r.group = entry.group;
|
||
}
|
||
|
||
// 2. 扫描 DialogueSequenceSO
|
||
foreach (var seq in AssetOperations.FindAll<DialogueSequenceSO>())
|
||
{
|
||
// variants[i].requiredFlags → 读取
|
||
if (seq.variants != null)
|
||
foreach (var v in seq.variants)
|
||
if (v.requiredFlags != null)
|
||
foreach (var fid in v.requiredFlags)
|
||
if (!string.IsNullOrEmpty(fid))
|
||
GetOrCreate(fid).readLocations.Add(($"对话变体条件 [{seq.name}]", seq));
|
||
|
||
// lines[i].choices[j].setWorldFlag → 设置
|
||
if (seq.lines != null)
|
||
foreach (var line in seq.lines)
|
||
if (line.choices != null)
|
||
foreach (var ch in line.choices)
|
||
if (!string.IsNullOrEmpty(ch.setWorldFlag))
|
||
GetOrCreate(ch.setWorldFlag).setLocations.Add(($"对话选项设置 [{seq.name}]", seq));
|
||
}
|
||
|
||
// 3. 扫描 QuestSO
|
||
foreach (var quest in AssetOperations.FindAll<QuestSO>())
|
||
{
|
||
// branches[i].conditionFlagEntries → 读取
|
||
if (quest.branches != null)
|
||
foreach (var branch in quest.branches)
|
||
if (branch.conditionFlagEntries != null)
|
||
foreach (var entry in branch.conditionFlagEntries)
|
||
if (!string.IsNullOrEmpty(entry.flagId))
|
||
GetOrCreate(entry.flagId).readLocations.Add(($"任务分支条件 [{quest.name}]", quest));
|
||
|
||
// prerequisites.flagCondition.flags → 读取
|
||
if (quest.prerequisites.flagCondition.flags != null)
|
||
foreach (var fid in quest.prerequisites.flagCondition.flags)
|
||
if (!string.IsNullOrEmpty(fid))
|
||
GetOrCreate(fid).readLocations.Add(($"任务前置标志 [{quest.name}]", quest));
|
||
}
|
||
|
||
// 4. 扫描 FlagSetCondition(EventChain 条件)→ 读取
|
||
foreach (var cond in AssetOperations.FindAll<FlagSetCondition>())
|
||
if (!string.IsNullOrEmpty(cond.flagId))
|
||
GetOrCreate(cond.flagId).readLocations.Add(($"链条件 [{cond.name}]", cond));
|
||
|
||
// 5. 扫描 SetFlagAction(EventChain 动作)→ 设置
|
||
foreach (var act in AssetOperations.FindAll<SetFlagAction>())
|
||
if (!string.IsNullOrEmpty(act.flagId))
|
||
GetOrCreate(act.flagId).setLocations.Add(($"链动作 [{act.name}]", act));
|
||
|
||
// 6. 扫描 NarrativeNPC 预制件中的 DialogueVersion 条件标志
|
||
// NarrativeNPC 是 MonoBehaviour,使用 SerializedObject 读取序列化字段以避免反射。
|
||
var prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets/_Game" });
|
||
foreach (var guid in prefabGuids)
|
||
{
|
||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||
if (prefab == null) continue;
|
||
foreach (var npc in prefab.GetComponentsInChildren<NarrativeNPC>(true))
|
||
{
|
||
var so = new SerializedObject(npc);
|
||
var vProp = so.FindProperty("_dialogueVersions");
|
||
if (vProp == null || !vProp.isArray) continue;
|
||
for (int i = 0; i < vProp.arraySize; i++)
|
||
{
|
||
var elem = vProp.GetArrayElementAtIndex(i);
|
||
var reqProp = elem.FindPropertyRelative("requiredFlags");
|
||
var blockProp = elem.FindPropertyRelative("blockedByFlags");
|
||
if (reqProp != null && reqProp.isArray)
|
||
for (int j = 0; j < reqProp.arraySize; j++)
|
||
{
|
||
string fid = reqProp.GetArrayElementAtIndex(j).stringValue;
|
||
if (!string.IsNullOrEmpty(fid))
|
||
GetOrCreate(fid).readLocations.Add(($"NPC版本条件 [{prefab.name}]", prefab));
|
||
}
|
||
if (blockProp != null && blockProp.isArray)
|
||
for (int j = 0; j < blockProp.arraySize; j++)
|
||
{
|
||
string fid = blockProp.GetArrayElementAtIndex(j).stringValue;
|
||
if (!string.IsNullOrEmpty(fid))
|
||
GetOrCreate(fid).readLocations.Add(($"NPC版本屏蔽 [{prefab.name}]", prefab));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 排序:未注册 → 孤立 → 正常,再按 ID 字典序
|
||
_records.Sort((a, b) =>
|
||
{
|
||
int pa = a.IsUnregistered ? 0 : a.IsOrphan ? 1 : 2;
|
||
int pb = b.IsUnregistered ? 0 : b.IsOrphan ? 1 : 2;
|
||
int c = pa.CompareTo(pb);
|
||
return c != 0 ? c : string.Compare(a.id, b.id, StringComparison.Ordinal);
|
||
});
|
||
|
||
RebuildList();
|
||
RebuildDetail();
|
||
}
|
||
|
||
// ── 列表重建 ─────────────────────────────────────────────────────────
|
||
|
||
private void RebuildList()
|
||
{
|
||
if (_listItems == null) return;
|
||
_listItems.Clear();
|
||
if (!_hasScanned) return;
|
||
|
||
int total = _records.Count;
|
||
int orphanCount = _records.Count(r => r.IsOrphan);
|
||
int unregCount = _records.Count(r => r.IsUnregistered);
|
||
if (_summaryLabel != null)
|
||
_summaryLabel.text = $"共 {total} 个标志 · 孤立 {orphanCount} · 未注册 {unregCount}";
|
||
|
||
foreach (var rec in _records)
|
||
{
|
||
if (_filterOrphan && !rec.IsOrphan) continue;
|
||
if (_filterUnregistered && !rec.IsUnregistered) continue;
|
||
|
||
bool isSelected = rec == _selected;
|
||
|
||
var row = new VisualElement();
|
||
row.style.flexDirection = FlexDirection.Row;
|
||
row.style.alignItems = Align.Center;
|
||
row.style.paddingTop = 3;
|
||
row.style.paddingBottom = 3;
|
||
row.style.paddingLeft = 8;
|
||
row.style.paddingRight = 8;
|
||
row.style.backgroundColor = isSelected
|
||
? new StyleColor(new Color(0.25f, 0.5f, 1f, 0.2f))
|
||
: StyleKeyword.None;
|
||
|
||
// 状态图标 + 颜色
|
||
string icon = rec.IsUnregistered ? "⚠" : rec.IsOrphan ? "○" : "●";
|
||
Color iconColor = rec.IsUnregistered
|
||
? new Color(1f, 0.4f, 0.2f)
|
||
: rec.IsOrphan
|
||
? new Color(1f, 0.85f, 0.1f)
|
||
: new Color(0.4f, 0.85f, 0.4f);
|
||
|
||
var iconLbl = new Label(icon);
|
||
iconLbl.style.fontSize = 10;
|
||
iconLbl.style.color = new StyleColor(iconColor);
|
||
iconLbl.style.width = 14;
|
||
iconLbl.style.flexShrink = 0;
|
||
row.Add(iconLbl);
|
||
|
||
var idLbl = new Label(rec.id);
|
||
idLbl.style.fontSize = 11;
|
||
idLbl.style.flexGrow = 1;
|
||
row.Add(idLbl);
|
||
|
||
// 使用次数徽章
|
||
if (rec.TotalUsages > 0)
|
||
{
|
||
var badge = new Label(rec.TotalUsages.ToString());
|
||
badge.style.fontSize = 9;
|
||
badge.style.opacity = 0.6f;
|
||
badge.style.paddingLeft = 4;
|
||
badge.style.paddingRight = 4;
|
||
badge.style.paddingTop = 1;
|
||
badge.style.paddingBottom = 1;
|
||
badge.style.borderTopLeftRadius = 8;
|
||
badge.style.borderTopRightRadius = 8;
|
||
badge.style.borderBottomLeftRadius = 8;
|
||
badge.style.borderBottomRightRadius = 8;
|
||
badge.style.backgroundColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.25f));
|
||
row.Add(badge);
|
||
}
|
||
|
||
var capturedRec = rec;
|
||
row.RegisterCallback<ClickEvent>(_ =>
|
||
{
|
||
_selected = capturedRec;
|
||
RebuildList();
|
||
RebuildDetail();
|
||
});
|
||
|
||
_listItems.Add(row);
|
||
}
|
||
}
|
||
|
||
// ── 详情重建 ─────────────────────────────────────────────────────────
|
||
|
||
private void RebuildDetail()
|
||
{
|
||
if (_detailRoot == null) return;
|
||
_detailRoot.Clear();
|
||
|
||
if (!_hasScanned)
|
||
{
|
||
var hint = new Label("请先点击「扫描标志使用情况」按钮。");
|
||
hint.style.opacity = 0.5f;
|
||
hint.style.marginTop = 24;
|
||
hint.style.unityTextAlign = TextAnchor.UpperCenter;
|
||
_detailRoot.Add(hint);
|
||
return;
|
||
}
|
||
|
||
if (_selected == null)
|
||
{
|
||
var hint = new Label("← 从左侧选择一个标志查看详情。");
|
||
hint.style.opacity = 0.5f;
|
||
hint.style.marginTop = 24;
|
||
hint.style.unityTextAlign = TextAnchor.UpperCenter;
|
||
_detailRoot.Add(hint);
|
||
return;
|
||
}
|
||
|
||
var r = _selected;
|
||
|
||
// 标题
|
||
var titleLbl = new Label(r.id);
|
||
titleLbl.style.fontSize = 15;
|
||
titleLbl.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||
titleLbl.style.paddingLeft = 12;
|
||
titleLbl.style.paddingTop = 12;
|
||
titleLbl.style.paddingBottom = 2;
|
||
_detailRoot.Add(titleLbl);
|
||
|
||
// 状态徽章
|
||
string statusText = r.IsUnregistered ? "⚠ 未在注册表中定义" : r.IsOrphan ? "○ 已注册但从未使用(孤立)" : "● 正常";
|
||
Color statusColor = r.IsUnregistered ? new Color(1f, 0.4f, 0.2f) : r.IsOrphan ? new Color(1f, 0.85f, 0.1f) : new Color(0.4f, 0.85f, 0.4f);
|
||
var statusLbl = new Label(statusText);
|
||
statusLbl.style.fontSize = 11;
|
||
statusLbl.style.color = new StyleColor(statusColor);
|
||
statusLbl.style.paddingLeft = 12;
|
||
statusLbl.style.marginBottom = 4;
|
||
_detailRoot.Add(statusLbl);
|
||
|
||
// "注册到注册表" 快捷按钮(仅未注册标志显示)
|
||
if (r.IsUnregistered)
|
||
{
|
||
var capturedRec = r;
|
||
var regBtn = new Button(() => RegisterFlagToRegistry(capturedRec))
|
||
{
|
||
text = "+ 注册到注册表",
|
||
tooltip = "将此标志 ID 追加到 WorldFlagRegistrySO.flags[] 中,并重新扫描。",
|
||
};
|
||
regBtn.style.marginLeft = 10;
|
||
regBtn.style.marginBottom = 6;
|
||
regBtn.style.width = 130;
|
||
_detailRoot.Add(regBtn);
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(r.group)) AddDetailRow("分组", r.group);
|
||
if (!string.IsNullOrEmpty(r.description)) AddDetailRow("描述", r.description);
|
||
|
||
_detailRoot.Add(SkillModule.MakeDivider());
|
||
|
||
AddLocationSection("📝 设置位置", r.setLocations, "无设置记录(标志只被读取,从不被写入)");
|
||
AddLocationSection("🔎 读取位置", r.readLocations, "无读取记录(标志只被写入,从不被读取)");
|
||
}
|
||
|
||
private void AddDetailRow(string label, string value)
|
||
{
|
||
var row = new VisualElement();
|
||
row.style.flexDirection = FlexDirection.Row;
|
||
row.style.paddingLeft = 12;
|
||
row.style.paddingBottom = 2;
|
||
|
||
var lbl = new Label($"{label}:");
|
||
lbl.style.fontSize = 11;
|
||
lbl.style.opacity = 0.55f;
|
||
lbl.style.width = 48;
|
||
lbl.style.flexShrink = 0;
|
||
|
||
var val = new Label(value);
|
||
val.style.fontSize = 11;
|
||
val.style.flexGrow = 1;
|
||
val.style.flexWrap = Wrap.Wrap;
|
||
|
||
row.Add(lbl);
|
||
row.Add(val);
|
||
_detailRoot.Add(row);
|
||
}
|
||
|
||
private void AddLocationSection(string sectionTitle, List<(string label, UnityEngine.Object asset)> locations, string emptyText)
|
||
{
|
||
var header = new Label(sectionTitle);
|
||
header.style.fontSize = 11;
|
||
header.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||
header.style.paddingLeft = 12;
|
||
header.style.paddingTop = 8;
|
||
header.style.paddingBottom = 3;
|
||
_detailRoot.Add(header);
|
||
|
||
if (locations.Count == 0)
|
||
{
|
||
var empty = new Label(emptyText);
|
||
empty.style.fontSize = 10;
|
||
empty.style.opacity = 0.45f;
|
||
empty.style.paddingLeft = 20;
|
||
empty.style.marginBottom = 4;
|
||
_detailRoot.Add(empty);
|
||
return;
|
||
}
|
||
|
||
foreach (var (lbl, asset) in locations)
|
||
{
|
||
var row = new VisualElement();
|
||
row.style.flexDirection = FlexDirection.Row;
|
||
row.style.alignItems = Align.Center;
|
||
row.style.paddingLeft = 14;
|
||
row.style.paddingRight = 8;
|
||
row.style.marginBottom = 2;
|
||
|
||
var caption = new Label(lbl);
|
||
caption.style.fontSize = 11;
|
||
caption.style.flexGrow = 1;
|
||
row.Add(caption);
|
||
|
||
if (asset != null)
|
||
{
|
||
var pingBtn = new Button(() =>
|
||
{
|
||
EditorGUIUtility.PingObject(asset);
|
||
Selection.activeObject = asset;
|
||
}) { text = "选中" };
|
||
pingBtn.style.fontSize = 10;
|
||
pingBtn.style.width = 36;
|
||
pingBtn.style.height = 18;
|
||
pingBtn.style.paddingTop = 0;
|
||
pingBtn.style.paddingBottom = 0;
|
||
row.Add(pingBtn);
|
||
}
|
||
|
||
_detailRoot.Add(row);
|
||
}
|
||
}
|
||
|
||
// ── 注册快捷操作 ──────────────────────────────────────────────────────
|
||
|
||
private void RegisterFlagToRegistry(FlagRecord rec)
|
||
{
|
||
var registry = WorldFlagRegistrySO.EditorInstance;
|
||
if (registry == null)
|
||
{
|
||
EditorUtility.DisplayDialog(
|
||
"注册表不存在",
|
||
"项目中未找到 WorldFlagRegistrySO 资产。\n" +
|
||
"请先通过 Create → BaseGames/Core/WorldFlagRegistry 创建注册表。",
|
||
"确定");
|
||
return;
|
||
}
|
||
|
||
// 检查是否已存在(理论上不可能,但防御性检查)
|
||
if (registry.flags != null)
|
||
{
|
||
foreach (var entry in registry.flags)
|
||
if (entry.id == rec.id) return;
|
||
}
|
||
|
||
var newEntry = new FlagEntry
|
||
{
|
||
id = rec.id,
|
||
description = "",
|
||
group = "",
|
||
};
|
||
|
||
var flags = registry.flags ?? System.Array.Empty<FlagEntry>();
|
||
var list = new System.Collections.Generic.List<FlagEntry>(flags) { newEntry };
|
||
Undo.RegisterCompleteObjectUndo(registry, $"注册标志 {rec.id}");
|
||
registry.flags = list.ToArray();
|
||
|
||
EditorUtility.SetDirty(registry);
|
||
AssetDatabase.SaveAssets();
|
||
|
||
// 将记录标记为已注册并重建 UI
|
||
rec.isRegistered = true;
|
||
RebuildList();
|
||
RebuildDetail();
|
||
|
||
Debug.Log($"[FlagAuditModule] 已将标志 '{rec.id}' 注册到 WorldFlagRegistrySO。");
|
||
}
|
||
}
|
||
}
|