using System; using System.Collections; using System.Collections.Generic; using AibisDream.Kit; using AibisDream.SaveSystem; using UnityEngine; using UnityEngine.Events; using AibisDream.Framework; using Yarn.Unity; namespace AibisDream { public class DialogController : Singleton { public event Action DialogueActivityChanged; private DialogueRunner _dialogueRunner; public DialogueRunner DialogueRunner => _dialogueRunner; [SerializeField] private LineAdvanceInput lineAdvanceInput; private string _currentNodeName; private string[] _currentNodeTags; private PendingExitCheckpoint _pendingExitCheckpoint; private long _dialogueFlowVersion; private long _dialogueRunId; private IDisposable _exitHandoffLock; internal long DialogueFlowVersion => _dialogueFlowVersion; internal long DialogueRunId => _dialogueRunId; private void Start() { _dialogueRunner = GetComponentInChildren(); _dialogueRunner.onDialogueStart ??= new UnityEvent(); _dialogueRunner.onDialogueComplete ??= new UnityEvent(); _dialogueRunner.onNodeStart ??= new UnityEventString(); _dialogueRunner.onNodeComplete ??= new UnityEventString(); _dialogueRunner.onDialogueStart.AddListener(OnDialogueStart); _dialogueRunner.onDialogueComplete.AddListener(OnDialogueComplete); _dialogueRunner.onNodeStart.AddListener(OnNodeStart); _dialogueRunner.onNodeComplete.AddListener(OnNodeComplete); } private void OnEnable() { SingleCastEventSystem.Global.Register(DialogEventEnum.StartNode, StartDialogNode); EnumEventSystem.Global.Register(GameLifecycleEvent.SessionStarted, ResetAdvanceMode); EnumEventSystem.Global.Register(GameLifecycleEvent.SessionEnded, ResetAdvanceMode); EnumEventSystem.Global.Register(DialogEventEnum.OptionShow, OnOptionShow); EnumEventSystem.Global.Register(DialogEventEnum.OptionSelected, OnOptionSelected); } private void OnDisable() { CancelPendingExitCheckpoint("DialogController disabled", logIfPending: false); SingleCastEventSystem.Global.Unregister(DialogEventEnum.StartNode); EnumEventSystem.Global.UnRegister(GameLifecycleEvent.SessionStarted, ResetAdvanceMode); EnumEventSystem.Global.UnRegister(GameLifecycleEvent.SessionEnded, ResetAdvanceMode); EnumEventSystem.Global.UnRegister(DialogEventEnum.OptionShow, OnOptionShow); EnumEventSystem.Global.UnRegister(DialogEventEnum.OptionSelected, OnOptionSelected); } private void ResetAdvanceMode() { advanceMode.Value = new AdvanceMode { isAuto = false, isQuick = false }; isInOption = false; lineSyncToken = null; Time.timeScale = 1f; } #region Yarn控制 /// /// 开启对话 /// /// Yarn组 public void StartDialog(YarnProject yarnProject) { CancelPendingExitCheckpoint("StartDialog 切换 YarnProject"); PrepareYarnProjectForDialog(yarnProject); _dialogueRunner.SetProject(yarnProject); _ = _dialogueRunner.StartDialogue("Start"); } public void StopDialog() { StartCoroutine(StopDialogRoutine()); } public IEnumerator StopDialogRoutine() { CancelPendingExitCheckpoint("StopDialogRoutine", logIfPending: false); if (_dialogueRunner != null && _dialogueRunner.IsDialogueRunning) { yield return _dialogueRunner.Stop(); } // Yarn Spinner runs IEnumerator commands as coroutines on the // DialogueRunner. Stopping the dialogue does not stop those detached // coroutines, so cancel them explicitly before a scene/session teardown. _dialogueRunner?.StopAllCoroutines(); SavePointEvaluator.ResetForProject(null); YarnVariableStorage.Instance.Clear(); DialogUIManager.Instance.HideDialog(); DialogueActivityChanged?.Invoke(false); } public void LoadDialog(YarnProject yarnProject) { CancelPendingExitCheckpoint("LoadDialog 切换 YarnProject"); PrepareYarnProjectForDialog(yarnProject); _dialogueRunner.SetProject(yarnProject); } private static void PrepareYarnProjectForDialog(YarnProject yarnProject) { var projectId = yarnProject != null ? yarnProject.name : null; SavePointEvaluator.ResetForProjectIfChanged(projectId); } public void StartDialogNode(string nodeName) { // 检查对话是否正在运行 if (_dialogueRunner.IsDialogueRunning) { Debug.LogWarning($"对话正在运行中,无法启动新节点: {nodeName}"); return; } if (CheckIfNodeExistInCurrentYarnProject(nodeName)) { _ = _dialogueRunner.StartDialogue(nodeName); } else { Debug.LogWarning($"节点 {nodeName} 不存在!"); } } private bool CheckIfNodeExistInCurrentYarnProject(string nodeName) { var currentProject = _dialogueRunner.YarnProject; if (currentProject == null) { Debug.LogWarning("当前没有可用的 YarnProject,无法检查节点"); return false; } string[] nodeNames = currentProject.NodeNames; if (Array.Exists(nodeNames, item => item == nodeName)) { return true; } else { Debug.Log($"节点 {nodeName} 不存在!"); return false; } } #endregion /// /// 返回当前正在执行的节点名与节点 tags;尚未启动 / 异常时返回 (null, 空数组)。 /// public (string nodeName, IReadOnlyList tags) GetCurrentNodeContext() { if (string.IsNullOrEmpty(_currentNodeName)) { return (null, Array.Empty()); } return (_currentNodeName, _currentNodeTags ?? Array.Empty()); } private void OnNodeStart(string nodeName) { _dialogueFlowVersion++; CancelPendingExitCheckpoint( $"节点 {nodeName} 已开始,说明之前的退出保存路径继续运行 Yarn", incrementFlowVersion: false); UpdateCurrentNodeContext(nodeName); var projectId = _dialogueRunner?.YarnProject?.name; var talkSceneId = GetCurrentTalkSceneId(); var tags = _currentNodeTags ?? Array.Empty(); if (SaveRestoreOrchestrator.IsRestoring) { SavePointEvaluator.OnRestoreEnterNode(projectId, nodeName); return; } if (!SavePointEvaluator.OnNodeStartForSavePolicy( projectId, nodeName, tags, out var policy, out var reason)) { if (reason == SavePointRejectReason.DetourResume) { Debug.Log($"[DialogController] 跳过 detour 返回续跑的自动存档: {nodeName}"); } return; } if (policy.Timing == NodeSaveTiming.DialogueExit) { _pendingExitCheckpoint = new PendingExitCheckpoint { ProjectId = projectId, TalkSceneId = talkSceneId, NodeName = nodeName, FlowVersion = _dialogueFlowVersion, DialogueRunId = _dialogueRunId }; return; } if (policy.Timing == NodeSaveTiming.NodeEnter) { StartCoroutine(SaveRestoreOrchestrator.AutoSaveRoutine( nodeName, projectId, talkSceneId, _dialogueRunId, _dialogueFlowVersion)); } } private void OnNodeComplete(string nodeName) { SavePointEvaluator.OnNodeComplete(_dialogueRunner?.YarnProject?.name, nodeName); if (_pendingExitCheckpoint != null && string.Equals(_pendingExitCheckpoint.NodeName, nodeName, StringComparison.Ordinal) && string.Equals( _pendingExitCheckpoint.ProjectId, _dialogueRunner?.YarnProject?.name, StringComparison.Ordinal) && _pendingExitCheckpoint.DialogueRunId == _dialogueRunId) { _pendingExitCheckpoint.IsNodeComplete = true; } ClearCurrentNodeContext(); } private void OnDialogueComplete() { var checkpoint = _pendingExitCheckpoint; _pendingExitCheckpoint = null; if (checkpoint?.IsNodeComplete == true) { _exitHandoffLock = EventSystemEx.Instance?.AcquireInteractionLock( $"exit checkpoint:{checkpoint.NodeName}"); } DialogueActivityChanged?.Invoke(false); EnumEventSystem.Global.Send(InteractionEventEnum.DialogEnd); ClearCurrentNodeContext(); if (checkpoint?.IsNodeComplete != true) { return; } StartCoroutine(SaveDialogueExitCheckpointRoutine(checkpoint)); } private void OnDialogueStart() { _dialogueRunId++; _dialogueFlowVersion++; DialogueActivityChanged?.Invoke(true); EnumEventSystem.Global.Send(InteractionEventEnum.DialogStart); } private IEnumerator SaveDialogueExitCheckpointRoutine(PendingExitCheckpoint checkpoint) { try { yield return SaveRestoreOrchestrator.DialogueExitSaveRoutine( checkpoint.NodeName, checkpoint.ProjectId, checkpoint.TalkSceneId, checkpoint.DialogueRunId, checkpoint.FlowVersion); } finally { ReleaseExitHandoffLock(); } } private void CancelPendingExitCheckpoint( string reason, bool logIfPending = true, bool incrementFlowVersion = true) { if (incrementFlowVersion) { _dialogueFlowVersion++; } ReleaseExitHandoffLock(); if (_pendingExitCheckpoint == null) { return; } if (logIfPending) { Debug.LogWarning( $"[DialogController] 取消 interaction/save_on_exit 退出存档: " + $"node={_pendingExitCheckpoint.NodeName}, reason={reason}"); } _pendingExitCheckpoint = null; } private void UpdateCurrentNodeContext(string nodeName) { if (_dialogueRunner == null || _dialogueRunner.Dialogue == null) { ClearCurrentNodeContext(); return; } _currentNodeName = nodeName; var tagsHeader = _dialogueRunner.Dialogue.GetHeaderValue(nodeName, "tags"); _currentNodeTags = string.IsNullOrWhiteSpace(tagsHeader) ? Array.Empty() : tagsHeader.Split(Array.Empty(), StringSplitOptions.RemoveEmptyEntries); } private void ClearCurrentNodeContext() { _currentNodeName = null; _currentNodeTags = null; } private void ReleaseExitHandoffLock() { _exitHandoffLock?.Dispose(); _exitHandoffLock = null; } private static string GetCurrentTalkSceneId() { return GameManager.Instance != null ? GameManager.Session.CurrentTalkScene?.name : null; } private sealed class PendingExitCheckpoint { public string ProjectId; public string TalkSceneId; public string NodeName; public long FlowVersion; public long DialogueRunId; public bool IsNodeComplete; } #region 推进方式修改 public BindProperty advanceMode = new(new AdvanceMode()); public void SwitchAutoMode() { var mode = advanceMode.Value; mode.isAuto = !mode.isAuto; advanceMode.Value = mode; } public void SwitchQuickMode() { var mode = advanceMode.Value; mode.isQuick = !mode.isQuick; advanceMode.Value = mode; Time.timeScale = advanceMode.Value.CurrentMode == AdvanceModeEnum.Quick ? 8f : 1f; } #endregion private LineSyncToken _lineSyncToken; public LineSyncToken lineSyncToken { get => _lineSyncToken; set { _lineSyncToken = value; lineAdvanceInput?.RunLine(value); } } public bool isInOption; public LineSyncState GetDialogState() { if (lineSyncToken != null && lineSyncToken.state != LineSyncState.Advanced) { return lineSyncToken.state; } else if (isInOption) { return LineSyncState.InOption; } else { return LineSyncState.Default; } } private void OnOptionShow() { isInOption = true; } private void OnOptionSelected(LineInfo lineInfo) { isInOption = false; } } public struct AdvanceMode { public bool isAuto; public bool isQuick; public AdvanceModeEnum CurrentMode { get { if (isQuick) { return AdvanceModeEnum.Quick; } else if (isAuto) { return AdvanceModeEnum.Auto; } else { return AdvanceModeEnum.Default; } } } } public enum AdvanceModeEnum { Default, Auto, Quick } }