2026-07-05 16:08:23 -04:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using Ichni.Story.UI;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.Events;
|
|
|
|
|
|
using Yarn.Unity;
|
|
|
|
|
|
using Yarn.Unity.UnityLocalization;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Ichni.Story.Dialogue
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 故事树 block 与 Yarn <see cref="DialogueRunner"/> 之间的桥接控制器。
|
|
|
|
|
|
/// <para>点击文本块 → 启动对应 Yarn 节点;对话结束 → 持久化剧情变量、标记 block 完成、
|
2026-07-21 15:24:42 -04:00
|
|
|
|
/// 重算解锁并存档、依次执行结束回调(例如歌曲解锁提示)。</para>
|
2026-07-05 16:08:23 -04:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class StoryDialogueController : MonoBehaviour
|
|
|
|
|
|
{
|
|
|
|
|
|
public static StoryDialogueController instance;
|
|
|
|
|
|
|
|
|
|
|
|
[Header("References")]
|
|
|
|
|
|
[Tooltip("驱动 Yarn 对话的 DialogueRunner。")]
|
|
|
|
|
|
public DialogueRunner dialogueRunner;
|
|
|
|
|
|
|
|
|
|
|
|
[Tooltip("故事树页面:对话进行时淡出、结束后淡入(可选)。")]
|
|
|
|
|
|
public StoryUIPage storyUIPage;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-07-21 15:24:42 -04:00
|
|
|
|
/// 对话结束后依次执行并清空的回调队列。供 <c>unlock_song</c> 等 Yarn 命令
|
2026-07-05 16:08:23 -04:00
|
|
|
|
/// 把"解锁提示"排队到对话结束后弹出。
|
|
|
|
|
|
/// </summary>
|
2026-07-21 15:24:42 -04:00
|
|
|
|
private readonly List<UnityAction> _dialogueEndActions = new List<UnityAction>();
|
2026-07-05 16:08:23 -04:00
|
|
|
|
|
|
|
|
|
|
// 当前正在播放对话的 block id;对话结束时据此标记完成
|
|
|
|
|
|
private string _activeBlockId;
|
|
|
|
|
|
|
2026-07-21 15:24:42 -04:00
|
|
|
|
// 当前 TextBlock 开始前的章节快照。它只服务于本次“中途退出对话”的恢复,
|
|
|
|
|
|
// 不会被写入 ES3,也不会影响歌曲成绩、解锁 Key、Settings 或其它章节。
|
|
|
|
|
|
private ChapterStorySave _activeDialogueSnapshot;
|
|
|
|
|
|
private string _activeDialogueChapterIndex;
|
|
|
|
|
|
private bool _isCancellingDialogue;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 当前对话对应的 TextBlock ID。Yarn 选项记忆与变量指令使用它定位所属章节 Block,
|
|
|
|
|
|
/// 不应由其它系统直接赋值。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public string ActiveBlockId => _activeBlockId;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 当前是否存在尚未提交的 TextBlock 对话事务。
|
|
|
|
|
|
/// Yarn 的全局副作用(例如 grant_unlock、unlock_song、show_message)必须据此决定延迟到正常结束后执行,
|
|
|
|
|
|
/// 从而避免玩家按 Back 时留下已解锁内容或孤立弹窗。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public bool IsDialogueTransactionActive =>
|
|
|
|
|
|
!string.IsNullOrEmpty(_activeBlockId) &&
|
|
|
|
|
|
GameSaveManager.instance?.StorySaveModule?.IsChapterTransactionActive(_activeDialogueChapterIndex) == true;
|
|
|
|
|
|
|
2026-07-05 16:08:23 -04:00
|
|
|
|
private void Awake()
|
|
|
|
|
|
{
|
|
|
|
|
|
instance = this;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-21 15:24:42 -04:00
|
|
|
|
private void Start()
|
|
|
|
|
|
{
|
|
|
|
|
|
// 这些引用不会由 StoryData 自检覆盖,因为它们属于场景运行时装配而非单个章节资产。
|
|
|
|
|
|
// 仅输出可定位 Warning,不阻断编辑器下的部分独立测试流程。
|
|
|
|
|
|
if (dialogueRunner == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning("[StoryDialogueController] 未配置 DialogueRunner;TextBlock 无法启动对话。", this);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (dialogueRunner.GetComponent<VNDialoguePresenter>() == null)
|
|
|
|
|
|
Debug.LogWarning("[StoryDialogueController] DialogueRunner 缺少 VNDialoguePresenter;台词和选项不会显示。", dialogueRunner);
|
|
|
|
|
|
|
|
|
|
|
|
if (dialogueRunner.GetComponent<UnityLocalisedLineProvider>() == null)
|
|
|
|
|
|
Debug.LogWarning("[StoryDialogueController] DialogueRunner 缺少 UnityLocalisedLineProvider;本地化台词无法解析。", dialogueRunner);
|
|
|
|
|
|
|
|
|
|
|
|
if (storyUIPage == null)
|
|
|
|
|
|
Debug.LogWarning("[StoryDialogueController] 未配置 StoryUIPage;对话开始/结束时不会切换剧情树页面。", this);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-05 16:08:23 -04:00
|
|
|
|
private void OnEnable()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (dialogueRunner != null && dialogueRunner.onDialogueComplete != null)
|
|
|
|
|
|
dialogueRunner.onDialogueComplete.AddListener(HandleDialogueComplete);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnDisable()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (dialogueRunner != null && dialogueRunner.onDialogueComplete != null)
|
|
|
|
|
|
dialogueRunner.onDialogueComplete.RemoveListener(HandleDialogueComplete);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-21 15:24:42 -04:00
|
|
|
|
private void OnDestroy()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (instance == this)
|
|
|
|
|
|
instance = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-05 16:08:23 -04:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 播放某文本块对应的 Yarn 节点。若对话已在进行、引用缺失或节点名为空则忽略。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void PlayBlock(TextBlockView block)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (block == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (dialogueRunner == null)
|
|
|
|
|
|
{
|
2026-07-24 17:56:30 -04:00
|
|
|
|
Debug.LogError("[StoryDialogueController] 未配置 DialogueRunner,无法开始对话!", this);
|
2026-07-05 16:08:23 -04:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 17:56:30 -04:00
|
|
|
|
if (dialogueRunner.IsDialogueRunning)
|
2026-07-05 16:08:23 -04:00
|
|
|
|
{
|
2026-07-24 17:56:30 -04:00
|
|
|
|
Debug.LogWarning("[StoryDialogueController] 已有对话正在运行,忽略本次点击。", dialogueRunner);
|
2026-07-05 16:08:23 -04:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrEmpty(block.YarnNodeName))
|
|
|
|
|
|
{
|
2026-07-24 17:56:30 -04:00
|
|
|
|
Debug.LogWarning(
|
|
|
|
|
|
$"[StoryDialogueController] Block '{block.blockId}' 未配置 Yarn Node,无法开始对话。",
|
|
|
|
|
|
block);
|
2026-07-05 16:08:23 -04:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 03:43:11 -04:00
|
|
|
|
// 将 DialogueRunner 的 YarnProject 对齐到当前章节,并验证场景中持久化的 LineProvider 配置。
|
2026-07-05 16:08:23 -04:00
|
|
|
|
EnsureProjectForCurrentChapter();
|
2026-07-24 03:43:11 -04:00
|
|
|
|
if (!ValidateUnityLocalisedLineProvider())
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-21 15:24:42 -04:00
|
|
|
|
// StartDialogue 对缺失 Node 只会在内部抛出/记录错误;必须在开启事务前预检,
|
|
|
|
|
|
// 否则会留下一个没有机会提交或回滚的半开事务。
|
|
|
|
|
|
if (dialogueRunner.YarnProject?.Program == null ||
|
|
|
|
|
|
!dialogueRunner.YarnProject.Program.Nodes.ContainsKey(block.YarnNodeName))
|
|
|
|
|
|
{
|
2026-07-24 17:56:30 -04:00
|
|
|
|
Debug.LogError(
|
|
|
|
|
|
$"[StoryDialogueController] Yarn Project 中不存在节点 '{block.YarnNodeName}'," +
|
|
|
|
|
|
$"已取消启动 Block '{block.blockId}'。",
|
|
|
|
|
|
dialogueRunner);
|
2026-07-21 15:24:42 -04:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
StoryTreeController treeController = StoryManager.instance?.treeController;
|
|
|
|
|
|
StorySaveModule saveModule = GameSaveManager.instance?.StorySaveModule;
|
|
|
|
|
|
string chapterIndex = treeController?.ActiveChapterIndex;
|
|
|
|
|
|
if (treeController == null || saveModule == null || string.IsNullOrEmpty(chapterIndex))
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError("[StoryDialogueController] 当前章节存档上下文未就绪,拒绝开始可回滚的 TextBlock 对话。");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 必须在任何 Yarn 命令、选项或 Marker 快照写入前完成复制并开启事务。
|
|
|
|
|
|
// 事务期间所有剧情变化仅停留在内存;正常结束才提交,中途 Back 则完整恢复本副本。
|
|
|
|
|
|
_activeDialogueSnapshot = StorySaveCloneUtility.CloneChapter(saveModule.GetChapter(chapterIndex));
|
|
|
|
|
|
_activeDialogueChapterIndex = chapterIndex;
|
|
|
|
|
|
if (!saveModule.BeginChapterTransaction(chapterIndex))
|
|
|
|
|
|
{
|
|
|
|
|
|
_activeDialogueSnapshot = null;
|
|
|
|
|
|
_activeDialogueChapterIndex = null;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-05 16:08:23 -04:00
|
|
|
|
_activeBlockId = block.blockId;
|
2026-07-21 15:24:42 -04:00
|
|
|
|
// 在 Yarn 命令或选项改变章节变量前先登记来源,并为 Marker 所在列创建快照。
|
|
|
|
|
|
StoryProgress.BeginBlockMutation(_activeBlockId);
|
2026-07-05 16:08:23 -04:00
|
|
|
|
|
2026-07-24 03:43:11 -04:00
|
|
|
|
storyUIPage?.FadeOut();
|
2026-07-24 17:56:30 -04:00
|
|
|
|
StartDialogueSafely(block.YarnNodeName).Forget();
|
2026-07-21 15:24:42 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-07-24 17:56:30 -04:00
|
|
|
|
/// 通过 Yarn Spinner 的公开 API 启动对话。台词表的定位与加载由
|
|
|
|
|
|
/// <see cref="UnityLocalisedLineProvider"/> 统一负责,项目代码不再重复持有或预加载同一张表。
|
|
|
|
|
|
/// 启动异常会恢复进入 TextBlock 前的章节快照,避免留下未提交事务。
|
2026-07-21 15:24:42 -04:00
|
|
|
|
/// </summary>
|
2026-07-24 17:56:30 -04:00
|
|
|
|
private async YarnTask StartDialogueSafely(string yarnNodeName)
|
2026-07-21 15:24:42 -04:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
await dialogueRunner.StartDialogue(yarnNodeName);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (System.Exception exception)
|
|
|
|
|
|
{
|
2026-07-24 03:43:11 -04:00
|
|
|
|
FailDialogueStartup("启动 Yarn 对话失败。", exception);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void FailDialogueStartup(string reason, System.Exception exception = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (exception != null)
|
2026-07-21 15:24:42 -04:00
|
|
|
|
Debug.LogException(exception, dialogueRunner);
|
2026-07-24 03:43:11 -04:00
|
|
|
|
|
2026-07-24 17:56:30 -04:00
|
|
|
|
Debug.LogError($"[StoryDialogueController] {reason}", dialogueRunner);
|
2026-07-24 03:43:11 -04:00
|
|
|
|
|
|
|
|
|
|
if (dialogueRunner != null && dialogueRunner.IsDialogueRunning)
|
|
|
|
|
|
{
|
|
|
|
|
|
dialogueRunner.GetComponent<VNDialoguePresenter>()?.CancelCurrentPresentation();
|
|
|
|
|
|
dialogueRunner.Stop().Forget();
|
2026-07-21 15:24:42 -04:00
|
|
|
|
}
|
2026-07-24 03:43:11 -04:00
|
|
|
|
|
|
|
|
|
|
RestoreInterruptedDialogue(reason);
|
2026-07-21 15:24:42 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 取消当前 TextBlock 对话并恢复进入前的章节剧情快照。
|
|
|
|
|
|
/// <para>Yarn 的 <see cref="DialogueRunner.Stop"/> 仍会触发 onDialogueComplete;
|
|
|
|
|
|
/// 因此此处先标记取消状态,完成回调会走恢复流程而不是误将 Block 标记为 Completed。</para>
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public bool CancelActiveDialogue()
|
|
|
|
|
|
{
|
2026-07-24 03:43:11 -04:00
|
|
|
|
if (_isCancellingDialogue || string.IsNullOrEmpty(_activeBlockId))
|
2026-07-21 15:24:42 -04:00
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 03:43:11 -04:00
|
|
|
|
if (dialogueRunner == null || !dialogueRunner.IsDialogueRunning)
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
2026-07-21 15:24:42 -04:00
|
|
|
|
_isCancellingDialogue = true;
|
|
|
|
|
|
_dialogueEndActions.Clear();
|
|
|
|
|
|
dialogueRunner.GetComponent<VNDialoguePresenter>()?.CancelCurrentPresentation();
|
|
|
|
|
|
dialogueRunner.Stop().Forget();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 将一个全局副作用排入“本次 TextBlock 成功提交后”执行的队列。
|
|
|
|
|
|
/// <para>没有活动对话事务时会立即执行,便于未来在非 TextBlock 环境复用同一批 Yarn 命令。</para>
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void ExecuteAfterDialogueCommit(UnityAction action)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (action == null)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
if (IsDialogueTransactionActive)
|
|
|
|
|
|
{
|
|
|
|
|
|
_dialogueEndActions.Add(action);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
action.Invoke();
|
2026-07-05 16:08:23 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 将 DialogueRunner 的 YarnProject 切换为当前已构建章节的 StoryData.yarnProject(若不同且未在运行)
|
|
|
|
|
|
private void EnsureProjectForCurrentChapter()
|
|
|
|
|
|
{
|
|
|
|
|
|
StoryTreeController tree = StoryManager.instance != null ? StoryManager.instance.treeController : null;
|
|
|
|
|
|
StoryData data = tree != null ? tree.ActiveStoryData : null;
|
|
|
|
|
|
|
|
|
|
|
|
if (data == null || data.yarnProject == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (dialogueRunner.YarnProject != data.yarnProject && !dialogueRunner.IsDialogueRunning)
|
|
|
|
|
|
dialogueRunner.SetProject(data.yarnProject);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-07-24 03:43:11 -04:00
|
|
|
|
/// 验证 DialogueRunner 序列化的 LineProvider 是否为 UnityLocalisedLineProvider。
|
|
|
|
|
|
/// <para>
|
|
|
|
|
|
/// LineProvider 应通过 Yarn Spinner Inspector 持久化配置;运行时不使用反射修改 Package 内部字段,
|
|
|
|
|
|
/// 这样 Editor、Mono 与 IL2CPP Build 使用完全相同的装配结果。
|
|
|
|
|
|
/// </para>
|
2026-07-05 16:08:23 -04:00
|
|
|
|
/// </summary>
|
2026-07-24 03:43:11 -04:00
|
|
|
|
private bool ValidateUnityLocalisedLineProvider()
|
2026-07-05 16:08:23 -04:00
|
|
|
|
{
|
2026-07-24 03:43:11 -04:00
|
|
|
|
if (dialogueRunner == null)
|
2026-07-05 16:08:23 -04:00
|
|
|
|
{
|
2026-07-24 03:43:11 -04:00
|
|
|
|
return false;
|
2026-07-05 16:08:23 -04:00
|
|
|
|
}
|
2026-07-24 03:43:11 -04:00
|
|
|
|
|
|
|
|
|
|
if (dialogueRunner.LineProvider is UnityLocalisedLineProvider)
|
2026-07-05 16:08:23 -04:00
|
|
|
|
{
|
2026-07-24 03:43:11 -04:00
|
|
|
|
return true;
|
2026-07-05 16:08:23 -04:00
|
|
|
|
}
|
2026-07-24 03:43:11 -04:00
|
|
|
|
|
|
|
|
|
|
Debug.LogError(
|
|
|
|
|
|
"[StoryDialogueController] DialogueRunner.lineProvider 未配置为 UnityLocalisedLineProvider。" +
|
|
|
|
|
|
"请在 Yarn Spinner Inspector 中使用 Unity Localization Line Provider,并配置台词 String Table。",
|
|
|
|
|
|
dialogueRunner);
|
|
|
|
|
|
return false;
|
2026-07-05 16:08:23 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void HandleDialogueComplete()
|
|
|
|
|
|
{
|
2026-07-21 15:24:42 -04:00
|
|
|
|
// Failed 流程已经清理 activeBlockId 后,底层 Runner 仍可能随后送达完成事件;此时绝不能误提交。
|
|
|
|
|
|
if (string.IsNullOrEmpty(_activeBlockId))
|
|
|
|
|
|
return;
|
2026-07-05 16:08:23 -04:00
|
|
|
|
|
2026-07-21 15:24:42 -04:00
|
|
|
|
if (_isCancellingDialogue)
|
|
|
|
|
|
{
|
|
|
|
|
|
RestoreCancelledDialogue();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 标记 Block 完成 → 重算解锁 → 保存完整章节容器(含变量、选项与回滚快照)。
|
2026-07-05 16:08:23 -04:00
|
|
|
|
if (!string.IsNullOrEmpty(_activeBlockId) &&
|
|
|
|
|
|
StoryManager.instance != null && StoryManager.instance.treeController != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
StoryManager.instance.treeController.OnBlockCompleted(_activeBlockId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-21 15:24:42 -04:00
|
|
|
|
StoryProgress.EndBlockMutation(_activeBlockId);
|
2026-07-05 16:08:23 -04:00
|
|
|
|
_activeBlockId = null;
|
|
|
|
|
|
|
2026-07-21 15:24:42 -04:00
|
|
|
|
// 所有本次 TextBlock 的变量、选项和完成状态至此才一次性写入 ES3。
|
|
|
|
|
|
GameSaveManager.instance?.StorySaveModule?.CommitChapterTransaction(_activeDialogueChapterIndex);
|
|
|
|
|
|
ClearDialogueSessionState();
|
|
|
|
|
|
|
2026-07-05 16:08:23 -04:00
|
|
|
|
// 依次执行并清空结束回调(如歌曲解锁提示)
|
2026-07-21 15:24:42 -04:00
|
|
|
|
if (_dialogueEndActions.Count > 0)
|
2026-07-05 16:08:23 -04:00
|
|
|
|
{
|
2026-07-21 15:24:42 -04:00
|
|
|
|
List<UnityAction> pending = new List<UnityAction>(_dialogueEndActions);
|
|
|
|
|
|
_dialogueEndActions.Clear();
|
2026-07-05 16:08:23 -04:00
|
|
|
|
foreach (UnityAction action in pending)
|
2026-07-07 01:28:27 -04:00
|
|
|
|
{
|
2026-07-05 16:08:23 -04:00
|
|
|
|
action?.Invoke();
|
2026-07-07 01:28:27 -04:00
|
|
|
|
}
|
2026-07-19 16:44:58 -04:00
|
|
|
|
// MessageUIPage 会在收到第一项消息/选项请求时自行开启统一队列与淡入,
|
|
|
|
|
|
// 此处不能再次直接 FadeIn,否则会与当前弹窗流程重复播放页面动画。
|
2026-07-05 16:08:23 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (storyUIPage != null)
|
2026-07-07 01:28:27 -04:00
|
|
|
|
{
|
2026-07-05 16:08:23 -04:00
|
|
|
|
storyUIPage.FadeIn();
|
2026-07-07 01:28:27 -04:00
|
|
|
|
}
|
2026-07-21 15:24:42 -04:00
|
|
|
|
|
|
|
|
|
|
DialogueHistory.Clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 处理 Back 触发的 Yarn 停止回调:放弃运行期间的全部章节剧情改动,
|
|
|
|
|
|
/// 并只刷新当前已实例化的 StoryTree,保留玩家拖动剧情树后的视口位置。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void RestoreCancelledDialogue()
|
|
|
|
|
|
{
|
|
|
|
|
|
RestoreInterruptedDialogue("玩家中途退出对话");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 取消和失败共用的事务回滚出口。该方法具有幂等性:无活动 Block 时直接返回,
|
|
|
|
|
|
/// 因而底层 Yarn 在 Stop/异常后重复触发完成事件也不会把已回滚的数据再次提交。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void RestoreInterruptedDialogue(string reason)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrEmpty(_activeBlockId))
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
string interruptedBlockId = _activeBlockId;
|
|
|
|
|
|
string chapterIndex = _activeDialogueChapterIndex;
|
|
|
|
|
|
StoryTreeController treeController = StoryManager.instance?.treeController;
|
|
|
|
|
|
|
|
|
|
|
|
Debug.LogWarning($"[StoryDialogueController] {reason},已回滚 TextBlock '{interruptedBlockId}' 的章节剧情事务。");
|
|
|
|
|
|
StoryProgress.EndBlockMutation(interruptedBlockId);
|
|
|
|
|
|
GameSaveManager.instance?.StorySaveModule?.RollbackChapterTransaction(
|
|
|
|
|
|
chapterIndex, _activeDialogueSnapshot);
|
|
|
|
|
|
treeController?.ReloadActiveChapterProgress();
|
|
|
|
|
|
|
|
|
|
|
|
_dialogueEndActions.Clear();
|
|
|
|
|
|
DialogueHistory.Clear();
|
|
|
|
|
|
_activeBlockId = null;
|
|
|
|
|
|
ClearDialogueSessionState();
|
|
|
|
|
|
storyUIPage?.FadeIn();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void ClearDialogueSessionState()
|
|
|
|
|
|
{
|
|
|
|
|
|
_activeDialogueSnapshot = null;
|
|
|
|
|
|
_activeDialogueChapterIndex = null;
|
|
|
|
|
|
_isCancellingDialogue = false;
|
2026-07-05 16:08:23 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|