Files
zeling_v2/Assets/_Game/Scripts/Editor/Modules/EventChainModule.cs
Joywayer 3c3ea1ead6 feat: Round 49 narrative systems improvements
QuestManager: extract CheckQuestDepsAndFlags shared method, simplify GetQuestLockInfo/CanAccept/MeetsPrerequisites; add GetQuestsInState+FilterQuests implementations; fix extra brace compile bug; add _pauseTimestamps logging; use actualDelta in ApplyAffinity event.

QuestSO: add depth>32 guard to HasPrerequisiteCycle and HasBranchCycle to prevent editor freeze on deep chains.

EventChainModule: replace FindObjectOfType with ServiceLocator.GetOrDefault in ForceExecute; add self-trigger flag detection (check 6) in ValidateAllChains using reflection.

DialogueVariantPreviewWindow: add matrix analysis section enumerating all 2^N flag combinations (N<=10) with table showing winning variant per combination.

WorldStateRegistry: LoadFromSave null guard on data.World sub-collections (P0 fix).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-25 00:17:27 +08:00

569 lines
24 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// DataHub 事件链模块 —— 管理 EventChainSO 资产。
/// 支持浏览、创建、删除、预览条件/动作列表,以及批量验证。
/// </summary>
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<EventChainSO> _listPane;
private DetailHeader _header;
private EventChainSO _selected;
// ── IDataModule ───────────────────────────────────────────────────────
public void Initialize()
{
_listPane = new SoListPane<EventChainSO>(
Folder, Prefix,
c => c.repeatable ? "可重复" : null);
// 扩展搜索chainId
_listPane.GetExtraSearchText = c => c.chainId;
}
public void BuildListPane(VisualElement container, Action<UnityEngine.Object> 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("⚠ nullInspector 中有空槽,请检查)");
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("⚠ nullInspector 中有空槽,请检查)");
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<EventChainSO>(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<EventChainManager>();
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<EventChainSO>();
if (allChains.Count == 0)
{
EditorUtility.DisplayDialog("事件链验证", "项目中未找到任何 EventChainSO。", "确定");
return;
}
var issues = new List<QuestValidationResultWindow.Issue>();
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<string, EventChainSO>(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<string>(StringComparer.Ordinal);
var readFlags = new System.Collections.Generic.HashSet<string>(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, "事件链批量验证结果", "事件链");
}
}
}