Story流程+本地化

This commit is contained in:
SoulliesOfficial
2026-07-24 17:56:30 -04:00
parent b0e0a7d5aa
commit de70870682
250 changed files with 5719 additions and 272966 deletions

View File

@@ -1,10 +1,7 @@
using System.Collections;
using System.Collections.Generic;
using Ichni.Story.UI;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Localization.Tables;
using UnityEngine.ResourceManagement.AsyncOperations;
using Yarn.Unity;
using Yarn.Unity.UnityLocalization;
@@ -40,9 +37,6 @@ namespace Ichni.Story.Dialogue
private ChapterStorySave _activeDialogueSnapshot;
private string _activeDialogueChapterIndex;
private bool _isCancellingDialogue;
private Coroutine _dialogueStartupCoroutine;
private const float DialogueTableLoadTimeoutSeconds = 30f;
/// <summary>
/// 当前对话对应的 TextBlock ID。Yarn 选项记忆与变量指令使用它定位所属章节 Block
@@ -109,23 +103,23 @@ namespace Ichni.Story.Dialogue
{
if (block == null) return;
Debug.Log($"[YarnDiag] StoryDialogueController.PlayBlock: block={block.blockId}, node={block.YarnNodeName}");
if (dialogueRunner == null)
{
Debug.LogError("[YarnDiag] [ERROR] 未配置 DialogueRunner无法开始对话");
Debug.LogError("[StoryDialogueController] 未配置 DialogueRunner无法开始对话", this);
return;
}
if (dialogueRunner.IsDialogueRunning || _dialogueStartupCoroutine != null)
if (dialogueRunner.IsDialogueRunning)
{
Debug.LogWarning("[YarnDiag] [WARNING] 已有对话正在运行或启动,忽略本次点击");
Debug.LogWarning("[StoryDialogueController] 已有对话正在运行,忽略本次点击。", dialogueRunner);
return;
}
if (string.IsNullOrEmpty(block.YarnNodeName))
{
Debug.LogWarning($"[YarnDiag] [WARNING] block '{block.blockId}' 未配置 yarnNodeName无法开始对话");
Debug.LogWarning(
$"[StoryDialogueController] Block '{block.blockId}' 未配置 Yarn Node无法开始对话。",
block);
return;
}
@@ -136,14 +130,15 @@ namespace Ichni.Story.Dialogue
return;
}
Debug.Log($"[YarnDiag] YarnProject={dialogueRunner.YarnProject?.name}, NodesCount={(dialogueRunner.YarnProject?.Program != null ? dialogueRunner.YarnProject.Program.Nodes.Count : 0)}, LineProviderType={dialogueRunner.LineProvider.GetType().FullName}");
// StartDialogue 对缺失 Node 只会在内部抛出/记录错误;必须在开启事务前预检,
// 否则会留下一个没有机会提交或回滚的半开事务。
if (dialogueRunner.YarnProject?.Program == null ||
!dialogueRunner.YarnProject.Program.Nodes.ContainsKey(block.YarnNodeName))
{
Debug.LogError($"[YarnDiag] [ERROR] Yarn Project 中不存在节点 '{block.YarnNodeName}',已取消启动 Block '{block.blockId}'", dialogueRunner);
Debug.LogError(
$"[StoryDialogueController] Yarn Project 中不存在节点 '{block.YarnNodeName}'" +
$"已取消启动 Block '{block.blockId}'。",
dialogueRunner);
return;
}
@@ -171,116 +166,20 @@ namespace Ichni.Story.Dialogue
// 在 Yarn 命令或选项改变章节变量前先登记来源,并为 Marker 所在列创建快照。
StoryProgress.BeginBlockMutation(_activeBlockId);
// Unity Localization 的 AsyncOperationHandle 原生支持协程等待。
// 先在项目侧显式加载当前章节台词表,再启动 Yarn不修改 Yarn Spinner Package
// 也不把 Unity Addressables Handle 转换为 YarnTask。
_dialogueStartupCoroutine = StartCoroutine(StartDialogueSafely(block.YarnNodeName));
}
/// <summary>
/// 使用 Unity Localization 的公开 API 加载当前章节台词表。
/// <para>
/// <see cref="UnityLocalisedLineProvider.PrepareForLinesAsync"/> 在 Yarn Spinner 3.2.1 中是 no-op
/// 不能用来预载 String Table因此这里直接等待 <see cref="UnityEngine.Localization.LocalizedStringTable.GetTableAsync"/>。
/// </para>
/// </summary>
private IEnumerator StartDialogueSafely(string yarnNodeName)
{
LocalizationBootstrap.BeginInitialization();
while (!LocalizationBootstrap.IsReady && !LocalizationBootstrap.HasFailed)
{
LocalizationBootstrap.RefreshState();
yield return null;
}
if (!LocalizationBootstrap.IsReady)
{
FailDialogueStartup($"Localization 初始化失败:{LocalizationBootstrap.FailureReason}");
yield break;
}
StoryData storyData = StoryManager.instance?.treeController?.ActiveStoryData;
if (storyData?.yarnLineStringTable == null || storyData.yarnLineStringTable.IsEmpty)
{
FailDialogueStartup(
$"章节 '{storyData?.chapterIndex ?? "<null>"}' 未配置 Yarn Line String Table。");
yield break;
}
AsyncOperationHandle<StringTable> tableOperation;
//Debug.Log(storyData.yarnLineStringTable.GetTable());
try
{
// LocalizedStringTable 会自动使用 LocalizationSettings.SelectedLocale。
// 这与 UnityLocalisedLineProvider 后续读取台词时使用的 Locale 保持一致。
Debug.Log($"[YarnDiag] 开始加载 Yarn 台词表。Table={storyData.yarnLineStringTable.TableReference}, Locale={UnityEngine.Localization.Settings.LocalizationSettings.SelectedLocale?.Identifier.Code ?? "<null>"}");
tableOperation = storyData.yarnLineStringTable.GetTableAsync();
}
catch (System.Exception exception)
{
FailDialogueStartup("创建 Yarn 台词表加载操作失败。", exception);
yield break;
}
Debug.Log(
$"[YarnDiag] 开始加载章节台词表。Chapter={storyData.chapterIndex}, " +
$"Table={storyData.yarnLineStringTable.TableReference}, " +
$"Locale={UnityEngine.Localization.Settings.LocalizationSettings.SelectedLocale?.Identifier.Code ?? "<null>"}");
float loadStartedAt = Time.realtimeSinceStartup;
float nextProgressLogAt = loadStartedAt + 5f;
while (!tableOperation.IsDone)
{
float elapsed = Time.realtimeSinceStartup - loadStartedAt;
if (elapsed >= DialogueTableLoadTimeoutSeconds)
{
FailDialogueStartup(
$"加载 Yarn 台词表超时。Table={storyData.yarnLineStringTable.TableReference}, " +
$"Elapsed={elapsed:F1}s, Percent={tableOperation.PercentComplete:P0}");
yield break;
}
if (Time.realtimeSinceStartup >= nextProgressLogAt)
{
Debug.LogWarning(
$"[YarnDiag] Yarn 台词表仍在加载。Elapsed={elapsed:F1}s, " +
$"Percent={tableOperation.PercentComplete:P0}");
nextProgressLogAt += 5f;
}
yield return null;
}
if (tableOperation.Status != AsyncOperationStatus.Succeeded || tableOperation.Result == null)
{
FailDialogueStartup(
$"Yarn 台词表加载失败。Table={storyData.yarnLineStringTable.TableReference}",
tableOperation.OperationException);
yield break;
}
Debug.Log(
$"[YarnDiag] 章节台词表加载完成。Locale={tableOperation.Result.LocaleIdentifier.Code}, " +
$"Entries={tableOperation.Result.Count}");
_dialogueStartupCoroutine = null;
storyUIPage?.FadeOut();
StartDialogueRunnerSafely(yarnNodeName).Forget();
StartDialogueSafely(block.YarnNodeName).Forget();
}
/// <summary>
/// 调用 Yarn Spinner 的公开 <see cref="DialogueRunner.StartDialogue"/> API。
/// 返回时表示所有 Presenter 已完成 OnDialogueStartedAsync且 Runner 已调用 Dialogue.Continue
/// 通过 Yarn Spinner 的公开 API 启动对话。台词表的定位与加载由
/// <see cref="UnityLocalisedLineProvider"/> 统一负责,项目代码不再重复持有或预加载同一张表
/// 启动异常会恢复进入 TextBlock 前的章节快照,避免留下未提交事务。
/// </summary>
private async YarnTask StartDialogueRunnerSafely(string yarnNodeName)
private async YarnTask StartDialogueSafely(string yarnNodeName)
{
try
{
Debug.Log($"[YarnDiag] 调用 DialogueRunner.StartDialogue。Node={yarnNodeName}");
await dialogueRunner.StartDialogue(yarnNodeName);
Debug.Log(
$"[YarnDiag] DialogueRunner 初始启动完成。Node={yarnNodeName}, " +
$"IsDialogueRunning={dialogueRunner.IsDialogueRunning}");
}
catch (System.Exception exception)
{
@@ -290,12 +189,10 @@ namespace Ichni.Story.Dialogue
private void FailDialogueStartup(string reason, System.Exception exception = null)
{
_dialogueStartupCoroutine = null;
if (exception != null)
Debug.LogException(exception, dialogueRunner);
Debug.LogError($"[YarnDiag] {reason}", dialogueRunner);
Debug.LogError($"[StoryDialogueController] {reason}", dialogueRunner);
if (dialogueRunner != null && dialogueRunner.IsDialogueRunning)
{
@@ -318,14 +215,6 @@ namespace Ichni.Story.Dialogue
return false;
}
if (_dialogueStartupCoroutine != null)
{
StopCoroutine(_dialogueStartupCoroutine);
_dialogueStartupCoroutine = null;
RestoreInterruptedDialogue("玩家在对话启动阶段中途退出");
return true;
}
if (dialogueRunner == null || !dialogueRunner.IsDialogueRunning)
return false;

View File

@@ -94,8 +94,6 @@ namespace Ichni.Story.Dialogue
public override YarnTask OnDialogueStartedAsync()
{
Debug.Log("[YarnDiag] VNDialoguePresenter.OnDialogueStartedAsync 开始。");
// 快进是一次 Yarn 对话会话的临时操作,绝不能遗留到下一次进入 TextBlock。
_isFastForwarding = false;
_typewriterComplete = false;
@@ -104,7 +102,6 @@ namespace Ichni.Story.Dialogue
DialogueHistory.BeginSession();
Page.PlayFadeIn();
Debug.Log("[YarnDiag] VNDialoguePresenter.OnDialogueStartedAsync 完成。");
return YarnTask.CompletedTask;
}
@@ -112,8 +109,6 @@ namespace Ichni.Story.Dialogue
{
if (Page == null) return;
Debug.Log($"[YarnDiag] VNDialoguePresenter.RunLineAsync: LineID='{line.TextID}', Speaker='{line.CharacterName}', Text='{line.TextWithoutCharacterName.Text}', RawText='{line.RawText}'");
// 确保选项区不可见;台词区始终可见
if (Page.choiceFrame != null) Page.choiceFrame.SetActive(false);
@@ -130,7 +125,8 @@ namespace Ichni.Story.Dialogue
if (string.IsNullOrEmpty(fullText) && !string.IsNullOrEmpty(line.RawText))
{
fullText = line.RawText;
Debug.LogWarning($"[YarnDiag] [WARNING] 台词 '{line.TextID}' 本地化解析结果为空,回退使用 RawText: '{fullText}'");
Debug.LogWarning(
$"[VNDialoguePresenter] 台词 '{line.TextID}' 的本地化结果为空,已回退使用 Yarn RawText。");
}
// 记录的是本次实际展示时已经解析完成的本地化文本,而非 Yarn Key。
DialogueHistory.AddLine(line.CharacterName, fullText);
@@ -291,37 +287,30 @@ namespace Ichni.Story.Dialogue
private void OnAdvanceButtonClicked()
{
Debug.Log($"[YarnDiag] OnAdvanceButtonClicked 推进按钮被点击_runner={(_runner != null ? _runner.name : "null")}, _typewriterComplete={_typewriterComplete}, _isFastForwarding={_isFastForwarding}, _optionTcs={(_optionTcs != null)}");
// 未记忆选项必须由 ChoiceButton 明确选择;推进按钮不能越过当前选择。
if (_optionTcs != null)
{
Debug.LogWarning("[YarnDiag] OnAdvanceButtonClicked 被拦截:正在等待选项选择。");
return;
}
// 如果玩家在快进时点击屏幕/推进键,则打断快进状态并停止当前动作
if (_isFastForwarding)
{
Debug.Log("[YarnDiag] OnAdvanceButtonClicked 打断快进模式。");
_isFastForwarding = false;
return;
}
if (_runner == null)
{
Debug.LogError("[YarnDiag] [ERROR] OnAdvanceButtonClicked 拦截_runner 为 null");
return;
}
if (!_typewriterComplete)
{
Debug.Log("[YarnDiag] RequestHurryUpLine (跳过打字效果)");
_runner.RequestHurryUpLine(); // 第一次点击:跳过打字效果
}
else
{
Debug.Log("[YarnDiag] RequestNextLine (推进下一行)");
_runner.RequestNextLine(); // 第二次点击:推进到下一行
}
}