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
{
///
/// DataHub 标志审计模块 —— 扫描项目所有 WorldStateFlag 引用,
/// 检测孤立标志(已注册但从未使用)和未注册标志(已使用但未在注册表定义)。
///
public class FlagAuditModule : IDataModule, IDataModuleOrdered
{
public string ModuleId => "flagaudit";
public string DisplayName => "标志审计";
public string IconName => "d_FilterByLabel";
public int DisplayOrder => 130;
// ── 数据 ─────────────────────────────────────────────────────────────
private readonly List _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 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(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())
{
// 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())
{
// 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())
if (!string.IsNullOrEmpty(cond.flagId))
GetOrCreate(cond.flagId).readLocations.Add(($"链条件 [{cond.name}]", cond));
// 5. 扫描 SetFlagAction(EventChain 动作)→ 设置
foreach (var act in AssetOperations.FindAll())
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(path);
if (prefab == null) continue;
foreach (var npc in prefab.GetComponentsInChildren(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(_ =>
{
_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();
var list = new System.Collections.Generic.List(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。");
}
}
}