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, "事件链批量验证结果", "事件链");
}
}
}