Files

482 lines
15 KiB
C#

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<DialogController>
{
public event Action<bool> 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>();
_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, string>(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, LineInfo>(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, LineInfo>(DialogEventEnum.OptionSelected, OnOptionSelected);
}
private void ResetAdvanceMode()
{
advanceMode.Value = new AdvanceMode
{
isAuto = false,
isQuick = false
};
isInOption = false;
lineSyncToken = null;
Time.timeScale = 1f;
}
#region Yarn控制
/// <summary>
/// 开启对话
/// </summary>
/// <param name="yarnProject">Yarn组</param>
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
/// <summary>
/// 返回当前正在执行的节点名与节点 tags;尚未启动 / 异常时返回 (null, 空数组)。
/// </summary>
public (string nodeName, IReadOnlyList<string> tags) GetCurrentNodeContext()
{
if (string.IsNullOrEmpty(_currentNodeName))
{
return (null, Array.Empty<string>());
}
return (_currentNodeName, _currentNodeTags ?? Array.Empty<string>());
}
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<string>();
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<string>()
: tagsHeader.Split(Array.Empty<char>(), 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> 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
}
}