using System; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using AibisDream.Framework; using AibisDream.Kit; using AibisDream.Utility; using UnityEngine; using Yarn.Unity; namespace AibisDream { public class LineSyncToken { private Action _nextStep; private readonly ITextShown _textShown; private readonly TaskCompletionSource _completion = new(TaskCreationOptions.RunContinuationsAsynchronously); public LineInfo lineInfo; public LineSyncState state; public CancellationTokenSource AdvanceDelayToken { get; private set; } public event Action SkipTextAnima; public event Action OnAdvance; public static LineSyncToken CreateAuto(LocalizedLine lineInfo, Action nextStep) { return new LineSyncToken(lineInfo, nextStep, new AutoNextTextShown()); } public static LineSyncToken CreateAuto(LocalizedLine lineInfo) { return new LineSyncToken(lineInfo, null, new AutoNextTextShown()); } public static LineSyncToken CreateImmediate(LocalizedLine lineInfo, Action nextStep) { return new LineSyncToken(lineInfo, nextStep, new ImmediateTextShown()); } public static LineSyncToken CreateImmediate(LocalizedLine lineInfo) { return new LineSyncToken(lineInfo, null, new ImmediateTextShown()); } public static LineSyncToken CreateDefault(LocalizedLine lineInfo, Action nextStep) { // 如果标记了 auto_next,使用自动推进逻辑 if (lineInfo.IsAutoSkipLine()) { return new LineSyncToken(lineInfo, nextStep, new AutoNextTextShown()); } return new LineSyncToken(lineInfo, nextStep, new DefaultTextShown()); } public static LineSyncToken CreateDefault(LocalizedLine lineInfo) { // 如果标记了 auto_next,使用自动推进逻辑 if (lineInfo.IsAutoSkipLine()) { return new LineSyncToken(lineInfo, null, new AutoNextTextShown()); } return new LineSyncToken(lineInfo, null, new DefaultTextShown()); } public LineSyncToken(LocalizedLine lineInfo, Action nextStep, ITextShown textShown) { this.lineInfo = LineInfo.Generate(lineInfo); _nextStep = nextStep; _textShown = textShown; } public void TrySkip() { if (state == LineSyncState.PlayText) { SkipTextAnima?.Invoke(); SkipTextAnima = null; } else { Debug.Log($"line{lineInfo.lineId} 状态为{state}, 不能跳过文本"); } } public void TryAdvance() { if (state == LineSyncState.WaitForAdvance || state == LineSyncState.WaitForAutoAdvance) { AdvanceDelayToken?.Cancel(); Advance(); Dispose(); } else { Debug.Log($"line{lineInfo.lineId} 状态为{state}, 不能向前推进"); } } public void ForceAdvance() { if (state != LineSyncState.Advanced) { // 先中断延时 AdvanceDelayToken?.Cancel(); // 推进 Advance(); Dispose(); } } private void Advance() { OnAdvance?.Invoke(); OnAdvance = null; _nextStep?.Invoke(); state = LineSyncState.Advanced; // LineEnd 表示当前行的所有推进回调已经完成,可安全替换其 UI 资源。 EnumEventSystem.Global.Send(DialogEventEnum.LineEnd); _completion.TrySetResult(true); } private void Dispose() { _nextStep = null; SkipTextAnima = null; } public async void TextShown() { // UI completion events can arrive more than once (for example, when a typewriter is // reused or a previous line finishes late). Only the active PlayText state is allowed // to enter the post-display delay; otherwise a stale callback could regress a token // from Advanced, or run before TextStart and corrupt the line lifecycle. if (state != LineSyncState.PlayText) { return; } AdvanceDelayToken = new CancellationTokenSource(); EnumEventSystem.Global.Send(DialogEventEnum.LineShown); await _textShown.OnTextShown(this); } public void TextStart() { if (state != LineSyncState.Default) { Debug.LogWarning($"line{lineInfo.lineId} 状态为{state}, 不能开始文本"); return; } state = LineSyncState.PlayText; EnumEventSystem.Global.Send(DialogEventEnum.LineStart, lineInfo); } public Task AwaitCompletion() { return _completion.Task; } } public struct LineInfo { public string lineId; public string lineText; public CharacterVo character; public bool isAutoSkip; public float? autoNextDelaySeconds; public bool isOption; public static LineInfo Generate(LocalizedLine dialogueLine) { float? autoNextDelaySeconds = null; if (dialogueLine.TryGetAutoNextDelaySeconds(out var parsedDelaySeconds)) { autoNextDelaySeconds = parsedDelaySeconds; } return new LineInfo { lineId = dialogueLine.TextID, lineText = dialogueLine.TextWithoutCharacterName.Text, character = dialogueLine.GetCharacterVo(), isAutoSkip = dialogueLine.IsAutoSkipLine(), autoNextDelaySeconds = autoNextDelaySeconds, isOption = false }; } public string GetRecordLine() { const string allowedTagsPattern = @"(<|{)(?!\/?(b|i|color)(?=>|\s|=))[^(>|})]+(>|})"; var res = Regex.Replace(lineText, allowedTagsPattern, ""); return res; } public int CalcAutoDelayTime() { var delayTime = lineText.RemoveRichTextTags().Length * ConstRef.CharacterDelayTime; return CommonUtil.Limit(delayTime, ConstRef.LineMaxDelay, ConstRef.LineMinDelay); } public int CalcDelayTime() { return ConstRef.FixedDelay; } public int CalcAutoNextDelayTime() { if (!autoNextDelaySeconds.HasValue) { return ConstRef.FixedDelay; } return (int)Math.Round( autoNextDelaySeconds.Value * 1000d, MidpointRounding.AwayFromZero); } } public interface ITextShown { Task OnTextShown(LineSyncToken token); } public class DefaultTextShown : ITextShown { public async Task OnTextShown(LineSyncToken token) { try { token.state = LineSyncState.AdvanceDelay; await Task.Delay(token.lineInfo.CalcDelayTime(), token.AdvanceDelayToken.Token); token.state = LineSyncState.WaitForAdvance; await Task.Delay(token.lineInfo.CalcAutoDelayTime(), token.AdvanceDelayToken.Token); token.state = LineSyncState.WaitForAutoAdvance; } catch (TaskCanceledException) { // 跳过,无事发生 } } } public class AutoNextTextShown : ITextShown { public async Task OnTextShown(LineSyncToken token) { try { token.state = LineSyncState.AdvanceDelay; await Task.Delay(token.lineInfo.CalcAutoNextDelayTime(), token.AdvanceDelayToken.Token); token.state = LineSyncState.WaitForAutoAdvance; token.TryAdvance(); } catch (TaskCanceledException) { // 跳过,无事发生 } } } public class ImmediateTextShown : ITextShown { public async Task OnTextShown(LineSyncToken token) { token.state = LineSyncState.WaitForAdvance; } } public enum LineSyncState { Default, PlayText, AdvanceDelay, WaitForAdvance, WaitForAutoAdvance, Advanced, // 仅限DialogController使用 InOption } }