feat: Round 48 narrative systems improvements
- QuestSO: Add ValidateBranchCycles() DFS detection for branches[].nextQuest loop - QuestSO: Mark three legacy prerequisite fields with v2.0 removal warning in Tooltip - IQuestManager: Add QuestLockReason enum + QuestLockInfo struct (strongly-typed lock info) - IQuestManager: Add GetQuestLockInfo() method to interface; GetQuestLockReason() now delegates to it - IQuestEventSource: Add OnQuestStateChanged(questId, oldState, newState) unified event - QuestManager: Implement GetQuestLockInfo(); fire OnQuestStateChanged on all state transitions - DialogueManager: Add one-frame yield in HandleChoices before ShowChoices (skip-debounce fix) - DialogueManager: Increment _playbackId in ForceEnd() to invalidate residual choice callbacks - DialogueSequenceSO: Add UNITY_EDITOR debug log in TryGetActiveVariant on variant match - WorldStateRegistry: Add OnBatchStateChanged event + BatchMark() batch-write API - DialogueModule: List badge shows warning indicator for unconditional-shadowing variants - DialogueModule: BuildVariantsCard shows logic mode (AND/OR) alongside flag conditions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
526
Assets/_Game/Scripts/Editor/Modules/EventChainModule.cs
Normal file
526
Assets/_Game/Scripts/Editor/Modules/EventChainModule.cs
Normal file
@@ -0,0 +1,526 @@
|
||||
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("⚠ 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<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 = UnityEngine.Object.FindObjectOfType<EventChainManager>();
|
||||
if (mgr == null)
|
||||
{
|
||||
Debug.LogWarning("[EventChainModule] 场景中未找到 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);
|
||||
}
|
||||
|
||||
Debug.Log($"[EventChainModule] 验证完成:{allChains.Count} 条事件链,{errorCount} 个错误,{warnCount} 个警告。");
|
||||
QuestValidationResultWindow.Show(issues, errorCount, warnCount, allChains.Count, "事件链批量验证结果", "事件链");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user