Files

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
}
}