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。"); } } }