using System; using System.Collections.Generic; using System.Linq; using System.Text; using UnityEditor; using UnityEngine; using UnityEngine.UIElements; using BaseGames.EventChain; using BaseGames.Editor.Shared; namespace BaseGames.Editor.Modules { /// /// DataHub 事件链模块 —— 管理 EventChainSO 资产。 /// 支持浏览、创建、删除、预览条件/动作列表,以及批量验证。 /// public class EventChainModule : IDataModule, IDataModuleOrdered { private const string Folder = "Assets/_Game/Data/EventChains"; private const string Prefix = "Chain_"; public string ModuleId => "eventchain"; public string DisplayName => "事件链"; public string IconName => "d_Animation.Play"; public int DisplayOrder => 120; private SoListPane _listPane; private DetailHeader _header; private EventChainSO _selected; // ── IDataModule ─────────────────────────────────────────────────────── public void Initialize() { _listPane = new SoListPane( Folder, Prefix, c => c.repeatable ? "可重复" : null); // 扩展搜索:chainId _listPane.GetExtraSearchText = c => c.chainId; } public void BuildListPane(VisualElement container, Action onSelected) { _listPane.SelectionChanged = sel => { _selected = sel; onSelected?.Invoke(sel); }; // 快速过滤标签行 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); bool filterRepeatable = false, filterNoCondition = false, filterNoAction = false; void RebuildFilter() { if (!filterRepeatable && !filterNoCondition && !filterNoAction) { _listPane.ExtraFilter = null; return; } _listPane.ExtraFilter = c => { if (filterRepeatable && !c.repeatable) return false; if (filterNoCondition && c.conditions != null && c.conditions.Length > 0) return false; if (filterNoAction && c.actions != null && c.actions.Length > 0) return false; return true; }; } filterRow.Add(QuestModule.MakeFilterChip("可重复", v => { filterRepeatable = v; RebuildFilter(); })); filterRow.Add(QuestModule.MakeFilterChip("无条件", v => { filterNoCondition = v; RebuildFilter(); })); filterRow.Add(QuestModule.MakeFilterChip("无动作", v => { filterNoAction = v; RebuildFilter(); })); container.Add(_listPane); _listPane.Refresh(); } public void BuildDetailPane(VisualElement container, UnityEngine.Object selected) { _selected = selected as EventChainSO; _header = new DetailHeader(); _header.SetAsset(_selected); _header.RenameRequested += OnRenameRequested; container.Add(_header); if (_selected == null) return; container.Add(BuildInfoCard(_selected)); container.Add(BuildConditionsList(_selected)); container.Add(BuildActionsList(_selected)); container.Add(BuildActionBar(_selected)); container.Add(SkillModule.MakeDivider()); container.Add(new UnityEditor.UIElements.InspectorElement(_selected)); } public void OnActivated() => _listPane?.Refresh(); // ── 内部 ────────────────────────────────────────────────────────────── private void OnRenameRequested(string newName) { if (_selected == null) return; var (ok, err) = AssetOperations.Rename(_selected, newName); if (!ok) EditorUtility.DisplayDialog("重命名失败", err, "确定"); else { _header.SetAsset(_selected); _listPane.Invalidate(); } } private static VisualElement BuildInfoCard(EventChainSO c) { var card = SkillModule.MakeCard(); SkillModule.AddChip(card, "Chain ID", string.IsNullOrEmpty(c.chainId) ? "(未设置)" : c.chainId); SkillModule.AddChip(card, "可重复", c.repeatable ? "是" : "否"); if (c.actionDelay > 0f) SkillModule.AddChip(card, "动作间隔", $"{c.actionDelay:F2}s"); int condCount = c.conditions?.Length ?? 0; int actCount = c.actions?.Length ?? 0; SkillModule.AddChip(card, "条件数量", condCount.ToString()); SkillModule.AddChip(card, "动作数量", actCount.ToString()); return card; } private static VisualElement BuildConditionsList(EventChainSO c) { var section = new VisualElement(); section.style.paddingLeft = 12; section.style.paddingRight = 12; section.style.paddingTop = 6; section.style.paddingBottom = 4; var title = new Label("触发条件"); title.style.unityFontStyleAndWeight = FontStyle.Bold; title.style.fontSize = 11; title.style.opacity = 0.8f; title.style.marginBottom = 4; section.Add(title); bool hasGroups = c.conditionGroups != null && c.conditionGroups.Length > 0; bool hasLegacy = c.conditions != null && c.conditions.Length > 0; if (!hasGroups && !hasLegacy) { var empty = new Label("(无条件,链将立即在首次 EvaluateAll 时触发)"); empty.style.opacity = 0.5f; empty.style.fontSize = 11; section.Add(empty); return section; } if (hasGroups) { // 新版 conditionGroups 展示 for (int g = 0; g < c.conditionGroups.Length; g++) { var group = c.conditionGroups[g]; // 组标题 var groupHeader = new VisualElement(); groupHeader.style.flexDirection = FlexDirection.Row; groupHeader.style.alignItems = Align.Center; groupHeader.style.marginBottom = 2; groupHeader.style.marginTop = g > 0 ? 4 : 0; var gLabel = new Label($"条件组 {g + 1}"); gLabel.style.fontSize = 11; gLabel.style.unityFontStyleAndWeight = FontStyle.Bold; gLabel.style.flexGrow = 1; groupHeader.Add(gLabel); var logicBadge = new Label(group.logic == BaseGames.Core.WorldStateFlagLogic.Or ? "Or(任一满足)" : "And(全部满足)"); logicBadge.style.fontSize = 10; logicBadge.style.opacity = 0.6f; groupHeader.Add(logicBadge); section.Add(groupHeader); if (group.conditions == null || group.conditions.Length == 0) { var noCondLabel = new Label(" (空组 — 无条件即视为满足)"); noCondLabel.style.opacity = 0.45f; noCondLabel.style.fontSize = 11; section.Add(noCondLabel); continue; } for (int i = 0; i < group.conditions.Length; i++) section.Add(BuildConditionRow(i, group.conditions[i], indent: true)); } } else { // 旧版 conditions[](隐式 And) var legacyNote = new Label("(旧版条件列表,隐式 And 逻辑;建议迁移至 conditionGroups)"); legacyNote.style.opacity = 0.45f; legacyNote.style.fontSize = 10; legacyNote.style.marginBottom = 2; section.Add(legacyNote); for (int i = 0; i < c.conditions.Length; i++) section.Add(BuildConditionRow(i, c.conditions[i], indent: false)); } // ── 运行时求值按钮(仅 Play Mode)────────────────────────────── if (Application.isPlaying) { section.Add(BuildRuntimeEvalPanel(c)); } return section; } private static VisualElement BuildConditionRow(int index, ChainCondition cond, bool indent) { var row = new VisualElement(); row.style.flexDirection = FlexDirection.Row; row.style.alignItems = Align.Center; row.style.marginBottom = 2; if (indent) row.style.paddingLeft = 12; var idx = new Label($"{index + 1}."); idx.style.width = 18; idx.style.opacity = 0.5f; idx.style.fontSize = 11; row.Add(idx); if (cond == null) { var warn = new Label("⚠ null(Inspector 中有空槽,请检查)"); warn.style.color = new StyleColor(new Color(0.9f, 0.5f, 0.2f)); warn.style.fontSize = 11; row.Add(warn); } else { // 运行时:直接显示 IsMet() 结果 if (Application.isPlaying) { bool met = cond.IsMet(); var statusIcon = new Label(met ? "✓" : "✗"); statusIcon.style.fontSize = 11; statusIcon.style.width = 14; statusIcon.style.color = new StyleColor(met ? new Color(0.2f, 0.75f, 0.35f) : new Color(0.85f, 0.35f, 0.30f)); row.Add(statusIcon); } var typeName = new Label(cond.GetType().Name); typeName.style.flexGrow = 1; typeName.style.fontSize = 11; row.Add(typeName); var ping = new Button(() => { EditorGUIUtility.PingObject(cond); Selection.activeObject = cond; }) { text = "定位" }; ping.style.fontSize = 10; ping.style.height = 18; row.Add(ping); } return row; } private static VisualElement BuildRuntimeEvalPanel(EventChainSO c) { var panel = new VisualElement(); panel.style.marginTop = 6; panel.style.paddingTop = 4; panel.style.paddingBottom = 4; panel.style.paddingLeft = 8; panel.style.paddingRight = 8; panel.style.backgroundColor = new StyleColor(new Color(0.15f, 0.22f, 0.15f, 1f)); var header = new VisualElement(); header.style.flexDirection = FlexDirection.Row; header.style.alignItems = Align.Center; header.style.marginBottom = 2; var title = new Label("▶ 运行时状态"); title.style.fontSize = 10; title.style.opacity = 0.7f; title.style.flexGrow = 1; title.style.unityFontStyleAndWeight = FontStyle.Bold; header.Add(title); var resultLabel = new Label(); resultLabel.style.fontSize = 10; void RefreshEval() { // 简单逐条 IsMet 求值用于显示(不触发实际执行) bool hasGroups = c.conditionGroups != null && c.conditionGroups.Length > 0; bool allPass = true; if (hasGroups) { foreach (var g in c.conditionGroups) { if (g.conditions == null || g.conditions.Length == 0) continue; bool groupPass = g.logic == BaseGames.Core.WorldStateFlagLogic.Or ? System.Array.Exists(g.conditions, x => x != null && x.IsMet()) : System.Array.TrueForAll(g.conditions, x => x == null || x.IsMet()); if (!groupPass) { allPass = false; break; } } } else if (c.conditions != null) { allPass = System.Array.TrueForAll(c.conditions, x => x == null || x.IsMet()); } resultLabel.text = allPass ? "✓ 当前满足触发条件" : "✗ 当前不满足触发条件"; resultLabel.style.color = new StyleColor(allPass ? new Color(0.2f, 0.75f, 0.35f) : new Color(0.85f, 0.35f, 0.30f)); } RefreshEval(); var refreshBtn = new Button(RefreshEval) { text = "刷新" }; refreshBtn.style.fontSize = 9; refreshBtn.style.height = 16; header.Add(refreshBtn); panel.Add(header); panel.Add(resultLabel); return panel; } private static VisualElement BuildActionsList(EventChainSO c) { var section = new VisualElement(); section.style.paddingLeft = 12; section.style.paddingRight = 12; section.style.paddingTop = 6; section.style.paddingBottom = 4; var title = new Label("执行动作"); title.style.unityFontStyleAndWeight = FontStyle.Bold; title.style.fontSize = 11; title.style.opacity = 0.8f; title.style.marginBottom = 4; section.Add(title); if (c.actions == null || c.actions.Length == 0) { var empty = new Label("(无动作,链触发后不执行任何操作)"); empty.style.opacity = 0.5f; empty.style.fontSize = 11; section.Add(empty); return section; } for (int i = 0; i < c.actions.Length; i++) { var act = c.actions[i]; var row = new VisualElement(); row.style.flexDirection = FlexDirection.Row; row.style.alignItems = Align.Center; row.style.marginBottom = 2; var idx = new Label($"{i + 1}."); idx.style.width = 18; idx.style.opacity = 0.5f; idx.style.fontSize = 11; row.Add(idx); if (act == null) { var warn = new Label("⚠ null(Inspector 中有空槽,请检查)"); warn.style.color = new StyleColor(new Color(0.9f, 0.5f, 0.2f)); warn.style.fontSize = 11; row.Add(warn); } else { var typeName = new Label($"{act.GetType().Name} {act.name}"); typeName.style.flexGrow = 1; typeName.style.fontSize = 11; row.Add(typeName); var ping = new Button(() => { EditorGUIUtility.PingObject(act); Selection.activeObject = act; }) { text = "定位" }; ping.style.fontSize = 10; ping.style.height = 18; row.Add(ping); } section.Add(row); } return section; } private VisualElement BuildActionBar(EventChainSO c) { var bar = SkillModule.MakeActionBar(); new Button(() => { AssetCreationWizard.Show(Folder, Prefix, (asset, id) => { asset.chainId = id; EditorUtility.SetDirty(asset); AssetDatabase.SaveAssets(); _listPane.Refresh(asset); }); }) { text = "创建" }.AlsoAddTo(bar); new Button(() => { EditorGUIUtility.PingObject(c); Selection.activeObject = c; }) { text = "定位" }.AlsoAddTo(bar); new Button(() => { string path = AssetDatabase.GetAssetPath(c); if (!string.IsNullOrEmpty(path)) EditorGUIUtility.systemCopyBuffer = path; }) { text = "复制路径" }.AlsoAddTo(bar); new Button(ValidateAllChains) { text = "批量验证" }.AlsoAddTo(bar); // 强制触发按钮仅在 Play Mode 下显示,用于调试绕过条件检查 if (Application.isPlaying) { var capturedC = c; var forceBtn = new Button(() => { var mgr = BaseGames.Core.ServiceLocator.GetOrDefault(); if (mgr == null) { Debug.LogWarning("[EventChainModule] ServiceLocator 中未找到 EventChainManager,无法强制触发。请确认场景中已挂载并注册。"); return; } Debug.Log($"[EventChainModule] 强制触发链:{capturedC.chainId}"); mgr.ForceExecute(capturedC.chainId); }) { text = "⚡ 强制触发" }; forceBtn.style.color = new StyleColor(new Color(1f, 0.75f, 0.2f)); forceBtn.AlsoAddTo(bar); } var del = new Button(() => { if (AssetOperations.Delete(c)) _listPane.Refresh(null); }) { text = "删除" }; SkillModule.ApplyDeleteStyle(del); del.AlsoAddTo(bar); return bar; } // ── 批量验证 ────────────────────────────────────────────────────────── private static void ValidateAllChains() { var allChains = AssetOperations.FindAll(); if (allChains.Count == 0) { EditorUtility.DisplayDialog("事件链验证", "项目中未找到任何 EventChainSO。", "确定"); return; } var issues = new List(); int errorCount = 0, warnCount = 0; void AddError(string msg, UnityEngine.Object asset) { issues.Add(new QuestValidationResultWindow.Issue { message = msg, isError = true, asset = asset }); errorCount++; } void AddWarn(string msg, UnityEngine.Object asset) { issues.Add(new QuestValidationResultWindow.Issue { message = msg, isError = false, asset = asset }); warnCount++; } // ① 空 chainId foreach (var c in allChains) if (string.IsNullOrWhiteSpace(c.chainId)) AddError($"{c.name}: chainId 为空,运行时无法存档或被 ChainCompletedCondition 引用。", c); // ② 重复 chainId var idMap = new Dictionary(StringComparer.Ordinal); foreach (var c in allChains) { if (string.IsNullOrWhiteSpace(c.chainId)) continue; if (!idMap.TryAdd(c.chainId, c)) AddError($"chainId '{c.chainId}' 重复({idMap[c.chainId].name} 与 {c.name}),运行时存档键将互串。", c); } // ③ 无动作 foreach (var c in allChains) { if (string.IsNullOrWhiteSpace(c.chainId)) continue; if (c.actions == null || c.actions.Length == 0) AddWarn($"{c.chainId}: actions 为空,链触发后不执行任何操作。", c); } // ④ conditions 含空槽 foreach (var c in allChains) { if (string.IsNullOrWhiteSpace(c.chainId) || c.conditions == null) continue; for (int i = 0; i < c.conditions.Length; i++) if (c.conditions[i] == null) AddError($"{c.chainId}: conditions[{i}] 为 null,运行时将触发 NullReferenceException。", c); } // ⑤ actions 含空槽 foreach (var c in allChains) { if (string.IsNullOrWhiteSpace(c.chainId) || c.actions == null) continue; for (int i = 0; i < c.actions.Length; i++) if (c.actions[i] == null) AddError($"{c.chainId}: actions[{i}] 为 null,运行时将触发 NullReferenceException。", c); } // ⑥ 自触发检测:某条件检查的标志由同一链的 Action 写入(可能造成链被自身条件阻断或无限反复触发) var flagFieldFlags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance; foreach (var c in allChains) { if (string.IsNullOrWhiteSpace(c.chainId)) continue; var writtenFlags = new System.Collections.Generic.HashSet(StringComparer.Ordinal); var readFlags = new System.Collections.Generic.HashSet(StringComparer.Ordinal); if (c.actions != null) foreach (var action in c.actions) { if (action == null) continue; foreach (var field in action.GetType().GetFields(flagFieldFlags)) if ((field.Name.Contains("flag", System.StringComparison.OrdinalIgnoreCase) || field.Name.Contains("Flag", System.StringComparison.OrdinalIgnoreCase)) && field.FieldType == typeof(string)) { var val = field.GetValue(action) as string; if (!string.IsNullOrEmpty(val)) writtenFlags.Add(val); } } if (c.conditions != null) foreach (var cond in c.conditions) { if (cond == null) continue; foreach (var field in cond.GetType().GetFields(flagFieldFlags)) if ((field.Name.Contains("flag", System.StringComparison.OrdinalIgnoreCase) || field.Name.Contains("Flag", System.StringComparison.OrdinalIgnoreCase)) && field.FieldType == typeof(string)) { var val = field.GetValue(cond) as string; if (!string.IsNullOrEmpty(val)) readFlags.Add(val); } } foreach (var flagId in readFlags) if (writtenFlags.Contains(flagId)) AddWarn($"{c.chainId}: 条件读取的标志 '{flagId}' 同时被本链的 Action 写入。" + "若该标志在触发前已被设置,链将永远无法执行或会产生意外的循环行为。", c); } Debug.Log($"[EventChainModule] 验证完成:{allChains.Count} 条事件链,{errorCount} 个错误,{warnCount} 个警告。"); QuestValidationResultWindow.Show(issues, errorCount, warnCount, allChains.Count, "事件链批量验证结果", "事件链"); } } }