Files
zeling_v2/Assets/_Game/Scripts/EventChain/EventChainManager.cs
Joywayer 6eaa83dc71 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>
2026-05-25 00:05:15 +08:00

358 lines
16 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;
using System.Collections.Generic;
using BaseGames.Core;
using BaseGames.Core.Events;
using BaseGames.Core.Save;
using UnityEngine;
namespace BaseGames.EventChain
{
/// <summary>
/// 世界事件链管理器(架构 14_NarrativeModule §10
/// 订阅游戏各系统的 SO 事件频道,转发为中继 C# 事件供 ChainCondition.Register() 绑定。
/// 每当收到新事件时,检查所有链的触发条件。
/// </summary>
public class EventChainManager : MonoBehaviour, ISaveable
{
[Header("所有事件链")]
[SerializeField] private EventChainSO[] _chains;
[Header("执行保护")]
[Tooltip("单个 Action 的最长执行时长unscaled time。超时后强制跳过并记录警告。0 = 不限时。")]
[Min(0f)]
[SerializeField] private float _maxActionTimeout = 30f;
[Header("事件频道(中继)")]
[SerializeField] private StringEventChannelSO _onBossDefeated; // EVT_EnemyDiedbossId
[SerializeField] private StringEventChannelSO _onCollectiblePickedUp; // EVT_CollectiblePickup
[SerializeField] private StringEventChannelSO _onAbilityUnlocked; // EVT_AbilityUnlocked
[SerializeField] private StringEventChannelSO _onRoomEntered; // EVT_SceneLoaded
[SerializeField] private StringEventChannelSO _onDialogueCompleted; // EVT_NpcDialogueCompleted
// ── 中继 C# 事件,供 ChainCondition.Register() 订阅 ──────────────
public event Action<string> OnBossDefeated;
public event Action<string> OnCollectiblePickedUp;
public event Action<string> OnAbilityUnlocked;
public event Action<string> OnRoomEntered;
public event Action<string> OnDialogueCompleted;
/// <summary>链执行完成时广播 chainId供 ChainCompletedCondition。</summary>
public event Action<string> OnChainCompleted;
/// <summary>
/// 世界标志变更事件(供 FlagSetCondition 事件驱动订阅,避免每帧轮询)。
/// 由 SetFlagAction.ExecuteAsync 在写入标志后调用 NotifyFlagChanged 触发。
/// </summary>
public event Action<string> OnWorldFlagChanged;
/// <summary>通知所有 FlagSetCondition 指定标志已变更,并立即重评估标志相关链条件。</summary>
public void NotifyFlagChanged(string flagId)
{
OnWorldFlagChanged?.Invoke(flagId);
EvaluateForMask(ChainEventMask.FlagChanged);
}
/// <summary>
/// 强制执行指定链,完全跳过条件检查。
/// 用于调试、关卡测试,或 QA 快速验证后续事件逻辑。
/// 仅在 Play Mode 中有效(链执行需要运行时环境)。
/// </summary>
public void ForceExecute(string chainId)
{
if (_chains == null)
{
Debug.LogWarning("[EventChainManager] ForceExecute: _chains 为空,没有可执行的事件链。", this);
return;
}
foreach (var chain in _chains)
{
if (chain == null || chain.chainId != chainId) continue;
StartCoroutine(ExecuteChain(chain));
return;
}
Debug.LogWarning($"[EventChainManager] ForceExecute: 未找到 chainId='{chainId}' 的事件链。", this);
}
#if UNITY_EDITOR
/// <summary>
/// Editor 专用静态事件:链执行完成时向编辑器窗口推送日志。
/// 仅在编辑器构建中存在,不产生运行时开销。
/// EventChainEditorWindow 在 OnEnable 中订阅此事件。
/// </summary>
public static event Action<string, string> OnChainExecutedInEditor;
#endif
private readonly HashSet<string> _completedChains = new();
private readonly CompositeDisposable _subs = new();
// 每个链对应的事件掩码OnEnable 后通过 BuildChainMasks 构建
private List<ChainMaskEntry> _chainMasks;
private struct ChainMaskEntry
{
public EventChainSO chain;
public ChainEventMask mask;
}
// ── 生命周期 ──────────────────────────────────────────────────────
private void Awake()
{
// 不在此处读取存档数据——LoadAsync 尚未完成_current 为 null。
// 已完成链的恢复由 ISaveable.OnLoad 在 LoadAsync 完成后处理。
}
private void OnEnable()
{
_onBossDefeated?.Subscribe(id => { OnBossDefeated?.Invoke(id); EvaluateForMask(ChainEventMask.Boss); }).AddTo(_subs);
_onCollectiblePickedUp?.Subscribe(id => { OnCollectiblePickedUp?.Invoke(id); EvaluateForMask(ChainEventMask.Collectible); }).AddTo(_subs);
_onAbilityUnlocked?.Subscribe(id => { OnAbilityUnlocked?.Invoke(id); EvaluateForMask(ChainEventMask.Ability); }).AddTo(_subs);
_onRoomEntered?.Subscribe(id => { OnRoomEntered?.Invoke(id); EvaluateForMask(ChainEventMask.Room); }).AddTo(_subs);
_onDialogueCompleted?.Subscribe(id => { OnDialogueCompleted?.Invoke(id); EvaluateForMask(ChainEventMask.Dialogue); }).AddTo(_subs);
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Register(this);
// 向每个 Condition 注册中继事件
// 先重置 SO 资产上的运行时状态,防止跨 PlayMode 会话或多场景加载时状态残留
if (_chains == null) return;
foreach (var chain in _chains)
{
// 旧版 conditions[] 字段
if (chain?.conditions != null)
foreach (var cond in chain.conditions)
{
cond?.ResetState();
cond?.Register(this);
}
// 新版 conditionGroups[] 字段
if (chain?.conditionGroups != null)
foreach (var group in chain.conditionGroups)
if (group?.conditions != null)
foreach (var cond in group.conditions)
{
cond?.ResetState();
cond?.Register(this);
}
}
BuildChainMasks();
}
private void OnDisable()
{
_subs.Clear();
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Unregister(this);
if (_chains == null) return;
foreach (var chain in _chains)
{
if (chain?.conditions != null)
foreach (var cond in chain.conditions)
cond?.Unregister(this);
if (chain?.conditionGroups != null)
foreach (var group in chain.conditionGroups)
if (group?.conditions != null)
foreach (var cond in group.conditions)
cond?.Unregister(this);
}
}
// ── ISaveable ─────────────────────────────────────────────────────────
/// <summary>
/// SetChainCompleted 通过 ISaveService 直接写入 SaveDataOnSave 无需操作。
/// </summary>
public void OnSave(SaveData data) { }
/// <summary>
/// LoadAsync 完成后由 GameSaveManager.Register 补发,恢复本会话前已完成的链。
/// 仅恢复状态为 "Completed" 的条目;其他状态(如未来扩展的 "Failed")不影响重入检查。
/// </summary>
public void OnLoad(SaveData data)
{
_completedChains.Clear();
if (data?.EventChains?.ChainStates == null) return;
foreach (var kv in data.EventChains.ChainStates)
if (kv.Value == "Completed")
_completedChains.Add(kv.Key);
}
// ── 评估逻辑 ──────────────────────────────────────────────────────
/// <summary>
/// 构建每条链的 ChainEventMask链内所有条件 RelevantEvents 的并集)。
/// OnEnable 在所有条件 Register 完毕后调用,之后事件触发只评估掩码相交的链。
/// </summary>
private void BuildChainMasks()
{
_chainMasks = new List<ChainMaskEntry>(_chains?.Length ?? 0);
if (_chains == null) return;
foreach (var chain in _chains)
{
if (chain == null) continue;
ChainEventMask mask = ChainEventMask.None;
bool hasGroups = chain.conditionGroups != null && chain.conditionGroups.Length > 0;
if (hasGroups)
{
foreach (var group in chain.conditionGroups)
if (group?.conditions != null)
foreach (var cond in group.conditions)
if (cond != null) mask |= cond.RelevantEvents;
}
else if (chain.conditions != null)
{
foreach (var cond in chain.conditions)
if (cond != null) mask |= cond.RelevantEvents;
}
// 无条件链:任何事件均可触发(等同 Any
if (mask == ChainEventMask.None) mask = ChainEventMask.Any;
_chainMasks.Add(new ChainMaskEntry { chain = chain, mask = mask });
}
}
/// <summary>
/// 懒评估:仅评估 mask 与 <paramref name="triggerMask"/> 相交的链。
/// ChainEventMask.Any-1与任何非零 mask 均相交,确保自定义条件链不被跳过。
/// </summary>
private void EvaluateForMask(ChainEventMask triggerMask)
{
if (_chainMasks == null) return;
foreach (var entry in _chainMasks)
{
if ((entry.mask & triggerMask) == 0) continue;
var chain = entry.chain;
if (!chain.repeatable && _completedChains.Contains(chain.chainId)) continue;
if (EvaluateChainConditions(chain)) StartCoroutine(ExecuteChain(chain));
}
}
/// <summary>
/// 评估链的所有触发条件。
/// 若链配置了 <see cref="EventChainSO.conditionGroups"/>,按组逻辑评估(组内 And/Or组间 And
/// 否则回退到旧版 <see cref="EventChainSO.conditions"/> 隐式 And 逻辑。
/// </summary>
private static bool EvaluateChainConditions(EventChainSO chain)
{
bool hasGroups = chain.conditionGroups != null && chain.conditionGroups.Length > 0;
if (hasGroups)
{
// 组间为 And所有组均须通过
foreach (var group in chain.conditionGroups)
{
if (group?.conditions == null || group.conditions.Length == 0) continue;
if (!EvaluateConditionGroup(group)) return false;
}
return true;
}
// 旧版conditions[] 隐式 And
if (chain.conditions != null)
foreach (var cond in chain.conditions)
if (cond != null && !cond.IsMet()) return false;
return true;
}
/// <summary>按组内 logicAnd/Or评估单个条件组是否通过。</summary>
private static bool EvaluateConditionGroup(ConditionGroup group)
{
if (group.logic == WorldStateFlagLogic.Or)
{
foreach (var cond in group.conditions)
if (cond != null && cond.IsMet()) return true;
return false;
}
// And默认
foreach (var cond in group.conditions)
if (cond != null && !cond.IsMet()) return false;
return true;
}
private IEnumerator ExecuteChain(EventChainSO chain)
{
// 防重入:一次性链立即标记为已完成
if (!chain.repeatable) _completedChains.Add(chain.chainId);
if (chain.actions != null)
foreach (var action in chain.actions)
{
if (action == null) continue;
bool actionFailed = false;
if (_maxActionTimeout > 0f)
{
bool finished = false;
Coroutine co = null;
try
{
co = StartCoroutine(SetTrueOnFinish(action.ExecuteAsync(this), () => finished = true));
}
catch (System.Exception ex)
{
Debug.LogError(
$"[EventChainManager] 链 '{chain.chainId}' 动作 '{action.GetType().Name}' 启动异常:{ex.Message}",
this);
actionFailed = true;
}
if (!actionFailed)
{
// 用 realtimeSinceStartup 计时,确保 PlayMode 暂停时超时仍能触发
float deadline = Time.realtimeSinceStartup + _maxActionTimeout;
while (!finished)
{
if (Time.realtimeSinceStartup >= deadline)
{
if (co != null) StopCoroutine(co);
Debug.LogWarning(
$"[EventChainManager] 链 '{chain.chainId}' 的动作 '{action.GetType().Name}' " +
$"执行超时({_maxActionTimeout}s已强制跳过。");
#if UNITY_EDITOR
OnChainExecutedInEditor?.Invoke(chain.chainId,
$"超时跳过:{action.GetType().Name}>{_maxActionTimeout}s");
#endif
break;
}
yield return null;
}
}
}
else
{
bool exceptionThrown = false;
IEnumerator routine = null;
try { routine = action.ExecuteAsync(this); }
catch (System.Exception ex)
{
Debug.LogError(
$"[EventChainManager] 链 '{chain.chainId}' 动作 '{action.GetType().Name}' 异常:{ex.Message}",
this);
exceptionThrown = true;
}
if (!exceptionThrown && routine != null)
yield return routine;
}
if (chain.actionDelay > 0f)
yield return new WaitForSeconds(chain.actionDelay);
}
ServiceLocator.GetOrDefault<ISaveService>()?.SetChainCompleted(chain.chainId);
OnChainCompleted?.Invoke(chain.chainId);
#if UNITY_EDITOR
OnChainExecutedInEditor?.Invoke(chain.chainId, "执行完成");
#endif
}
/// <summary>
/// 将 inner 协程执行完毕后触发 onFinish 回调,供超时保护使用。
/// StopCoroutine 此协程时,内嵌的 inner通过 yield return 直接展开)也会一并停止。
/// </summary>
private static IEnumerator SetTrueOnFinish(IEnumerator inner, Action onFinish)
{
yield return inner;
onFinish();
}
}
}