using System; using System.Threading; using BriarQueen.Data.Identifiers; using BriarQueen.Framework.Coordinators.Events; using BriarQueen.Framework.Events.Audio; using BriarQueen.Framework.Events.UI; using Cysharp.Threading.Tasks; using VContainer; namespace BriarQueen.Framework.Services.Subtitles { public class SubtitleService : IDisposable { private readonly EventCoordinator _eventCoordinator; private CancellationTokenSource _subtitleCts; private SubtitleKey _activeSubtitleKey = SubtitleKey.None; private string _currentText = string.Empty; public bool IsVisible => _activeSubtitleKey != SubtitleKey.None && !string.IsNullOrWhiteSpace(_currentText); public string CurrentText => _currentText; [Inject] public SubtitleService(EventCoordinator eventCoordinator) { _eventCoordinator = eventCoordinator; } public void Initialize() { _eventCoordinator.Subscribe(OnVoicePlaybackStarted); _eventCoordinator.Subscribe(OnVoicePlaybackFinished); } public void Dispose() { _eventCoordinator.Unsubscribe(OnVoicePlaybackStarted); _eventCoordinator.Unsubscribe(OnVoicePlaybackFinished); CancelCurrentSubtitle(); ClearSubtitle(); } public void PlayScriptedSubtitle(SubtitleKey subtitleKey, float durationOverrideSeconds = 0f) { if (subtitleKey == SubtitleKey.None) { ClearScriptedSubtitle(); return; } if (!SubtitleIdentifiers.TryGet(subtitleKey, out var entry) || string.IsNullOrWhiteSpace(entry.Text)) return; var duration = durationOverrideSeconds > 0f ? durationOverrideSeconds : entry.PreferredDurationSeconds; ShowSubtitle(subtitleKey, entry.Text, duration).Forget(); } public void ClearScriptedSubtitle() { CancelCurrentSubtitle(); ClearSubtitle(); } private void OnVoicePlaybackStarted(VoicePlaybackStartedEvent evt) { if (evt.SubtitleKey == SubtitleKey.None) return; if (!SubtitleIdentifiers.TryGet(evt.SubtitleKey, out var entry) || string.IsNullOrWhiteSpace(entry.Text)) return; var duration = entry.PreferredDurationSeconds > 0f ? entry.PreferredDurationSeconds : evt.ClipLengthSeconds; ShowSubtitle(evt.SubtitleKey, entry.Text, duration).Forget(); } private void OnVoicePlaybackFinished(VoicePlaybackFinishedEvent evt) { if (evt.SubtitleKey == SubtitleKey.None) return; if (_activeSubtitleKey != evt.SubtitleKey) return; CancelCurrentSubtitle(); ClearSubtitle(); } private async UniTaskVoid ShowSubtitle(SubtitleKey subtitleKey, string text, float durationSeconds) { CancelCurrentSubtitle(); _activeSubtitleKey = subtitleKey; _currentText = text; _subtitleCts = new CancellationTokenSource(); _eventCoordinator.PublishImmediate(new SubtitleDisplayChangedEvent(text, true)); var safeDuration = Math.Max(0f, durationSeconds); if (safeDuration <= 0f) return; try { await UniTask.Delay( TimeSpan.FromSeconds(safeDuration), DelayType.UnscaledDeltaTime, cancellationToken: _subtitleCts.Token); } catch (OperationCanceledException) { return; } if (_activeSubtitleKey == subtitleKey) ClearSubtitle(); } private void CancelCurrentSubtitle() { if (_subtitleCts == null) return; try { _subtitleCts.Cancel(); } catch { } _subtitleCts.Dispose(); _subtitleCts = null; } private void ClearSubtitle() { _activeSubtitleKey = SubtitleKey.None; _currentText = string.Empty; _eventCoordinator.PublishImmediate(new SubtitleDisplayChangedEvent(string.Empty, false)); } } }