285 lines
8.7 KiB
C#
285 lines
8.7 KiB
C#
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<bool> _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
|
|
}
|
|
}
|