using System; using System.Collections.Generic; using DataTable; using Definition; using Definition.Enum; using Event; using GameFramework.DataTable; using GameFramework.Event; using UI; using UnityEngine; using UnityGameFramework.Runtime; namespace CustomComponent { [DisallowMultipleComponent] public class DialogComponent : GameFrameworkComponent { [Serializable] private sealed class SpeakerPortraitBinding { [SerializeField] private string _speakerId = string.Empty; [SerializeField] private List _portraits = new List(); public string SpeakerId => _speakerId; public List Portraits => _portraits; } #region Property private const int DialogChapterDivisor = 1000; private const int LineChapterDivisor = 100000000; private const int LineDialogDivisor = 100000; [Header("角色立绘配置(拖拽 Sprite)")] [SerializeField] private List _speakerPortraitBindings = new List(); private readonly Dictionary _dialogMap = new(); private readonly Dictionary> _dialogLinesMap = new(); private readonly Dictionary _dialogFirstLineIdMap = new(); private static readonly HashSet NonPortraitSpeakerIds = new(System.StringComparer.OrdinalIgnoreCase) { "Other", "Subtitle", "Time" }; private IDataTable _dtDialog; private IDataTable _dtDialogLine; private DialogFormController _formController; private DialogFormContext _formContext; private int _pendingStartDialogId; private int _activeChapterTitleRequestId; private int _nextChapterTitleRequestId = 1; private bool _chapterTitleShownAfterInit; [Header("章节标题过场")] [SerializeField] [Min(0f)] private float _chapterTitleDimFadeInDuration = 0.2f; [SerializeField] [Min(0f)] private float _chapterTitlePreBlackDuration = 0.25f; [SerializeField] [Min(0f)] private float _chapterTitleTitleFadeInDelay = 0f; [SerializeField] [Min(0f)] private float _chapterTitleFadeInDuration = 0.3f; [SerializeField] [Min(0f)] private float _chapterTitleHoldDuration = 1.2f; [SerializeField] [Min(0f)] private float _chapterTitleFadeOutDuration = 0.3f; [SerializeField] [Min(0f)] private float _chapterTitleDimFadeOutDuration = 0.2f; private int _currentChapterId; private int _currentLineIndex = -1; private bool _isInitialized; private bool _isPlaying; public bool IsInitialized => _isInitialized; public bool IsPlaying => _isPlaying; public int CurrentChapterId => _currentChapterId; public int CurrentDialogId => _formContext != null ? _formContext.DialogId : 0; public int CurrentLineId => _formContext != null ? _formContext.CurrentLineId : 0; public int GetSpeakerPortraitCount(string speakerId) { if (string.IsNullOrWhiteSpace(speakerId)) { return 0; } SpeakerPortraitBinding binding = FindSpeakerPortraitBinding(speakerId); if (binding == null || binding.Portraits == null) { return 0; } int validCount = 0; for (int i = 0; i < binding.Portraits.Count; i++) { if (binding.Portraits[i] != null) { validCount++; } } return validCount; } #endregion private void Start() { GameEntry.Event.Subscribe(DialogNextLineRequestEventArgs.EventId, OnDialogNextLineRequest); GameEntry.Event.Subscribe(DialogSkipRequestEventArgs.EventId, OnDialogSkipRequest); GameEntry.Event.Subscribe(DialogStopRequestEventArgs.EventId, OnDialogStopRequest); GameEntry.Event.Subscribe(ChapterTitleCompletedEventArgs.EventId, OnChapterTitleCompleted); } private void OnDestroy() { GameEntry.Event.Unsubscribe(DialogNextLineRequestEventArgs.EventId, OnDialogNextLineRequest); GameEntry.Event.Unsubscribe(DialogSkipRequestEventArgs.EventId, OnDialogSkipRequest); GameEntry.Event.Unsubscribe(DialogStopRequestEventArgs.EventId, OnDialogStopRequest); GameEntry.Event.Unsubscribe(ChapterTitleCompletedEventArgs.EventId, OnChapterTitleCompleted); } public bool Init(int chapterId) { if (chapterId <= 0) { Log.Warning("Dialog init failed. chapterId must be positive."); return false; } if (!EnsureDataTables()) { return false; } StopDialog(); _dialogMap.Clear(); _dialogLinesMap.Clear(); _dialogFirstLineIdMap.Clear(); DRDialog[] dialogRows = _dtDialog.GetDataRows((a, b) => a.Id.CompareTo(b.Id)); for (int i = 0; i < dialogRows.Length; i++) { DRDialog dialogRow = dialogRows[i]; if (dialogRow == null) { continue; } if (ParseChapterIdFromDialogId(dialogRow.Id) != chapterId) { continue; } _dialogMap[dialogRow.Id] = dialogRow; } if (_dialogMap.Count == 0) { Log.Warning("Dialog init failed. No dialog rows found for chapter '{0}'.", chapterId.ToString()); return false; } DRDialogLine[] lineRows = _dtDialogLine.GetDataRows((a, b) => a.Id.CompareTo(b.Id)); foreach (var lineRow in lineRows) { if (lineRow == null) { continue; } if (ParseChapterIdFromLineId(lineRow.Id) != chapterId) { continue; } int dialogId = ParseDialogIdFromLineId(lineRow.Id); if (!_dialogMap.ContainsKey(dialogId)) { continue; } if (!_dialogLinesMap.TryGetValue(dialogId, out List dialogLines)) { dialogLines = new List(); _dialogLinesMap.Add(dialogId, dialogLines); } dialogLines.Add(lineRow); } List invalidDialogIds = new List(); foreach (var dialogPair in _dialogMap) { if (!_dialogLinesMap.TryGetValue(dialogPair.Key, out List dialogLines) || dialogLines.Count == 0) { Log.Warning("Dialog init warning. Dialog '{0}' has no lines and will be ignored.", dialogPair.Key.ToString()); invalidDialogIds.Add(dialogPair.Key); continue; } dialogLines.Sort((a, b) => a.Id.CompareTo(b.Id)); _dialogFirstLineIdMap[dialogPair.Key] = dialogLines[0].Id; } foreach (var id in invalidDialogIds) { _dialogMap.Remove(id); } if (_dialogMap.Count == 0) { Log.Warning("Dialog init failed. No valid dialog remains for chapter '{0}'.", chapterId.ToString()); return false; } PreloadChapterPortraitSprites(); EnsureFormController(); _formContext = null; _currentChapterId = chapterId; _currentLineIndex = -1; _pendingStartDialogId = 0; _activeChapterTitleRequestId = 0; _chapterTitleShownAfterInit = false; _isInitialized = true; _isPlaying = false; return true; } public bool StartDialog(int dialogId) { if (!_isInitialized) { Log.Warning("Start dialog failed. Dialog component is not initialized."); return false; } if (ParseChapterIdFromDialogId(dialogId) != _currentChapterId) { Log.Warning("Start dialog failed. Dialog '{0}' does not belong to chapter '{1}'.", dialogId.ToString(), _currentChapterId.ToString()); return false; } if (!_dialogMap.TryGetValue(dialogId, out DRDialog dialogRow)) { Log.Warning("Start dialog failed. Dialog '{0}' was not found in current chapter cache.", dialogId.ToString()); return false; } if (!_dialogLinesMap.TryGetValue(dialogId, out List dialogLines) || dialogLines.Count == 0) { Log.Warning("Start dialog failed. Dialog '{0}' has no playable lines.", dialogId.ToString()); return false; } if (_activeChapterTitleRequestId > 0) { _pendingStartDialogId = dialogId; return true; } if (!_chapterTitleShownAfterInit) { _chapterTitleShownAfterInit = true; _pendingStartDialogId = dialogId; if (TryOpenChapterTitle(dialogRow)) { return true; } _pendingStartDialogId = 0; } if (_isPlaying) { StopDialog(); } EnsureFormController(); if (_formContext == null) { _formContext = new DialogFormContext(); } _formContext.ChapterId = _currentChapterId; _formContext.DialogId = dialogRow.Id; _formContext.DialogTitle = dialogRow.Title; _formContext.DialogUIMode = dialogRow.UIMode; _formContext.PlayingSpeed = (DialogPlayingSpeed)GameEntry.Setting.GetInt(Constant.Setting.DialogPlayingSpeed); _formContext.DialogWindowAlpha = (DialogWindowAlpha)GameEntry.Setting.GetInt(Constant.Setting.DialogWindowAlpha); _currentLineIndex = 0; ApplyLineToContext(dialogLines[_currentLineIndex], _currentLineIndex, dialogLines.Count); _isPlaying = true; _formController.OpenUI(_formContext); _formController.OnDialogStarted(_formContext); _formController.OnDialogLineChanged(_formContext); return true; } public bool NextLine() { if (!_isPlaying) { Log.Warning("Next line failed. No dialog is playing."); return false; } if (!TryGetCurrentDialogLines(out List dialogLines)) { StopDialog(); return false; } int nextLineIndex = _currentLineIndex + 1; if (nextLineIndex >= dialogLines.Count) { EndDialogInternal(); return true; } _currentLineIndex = nextLineIndex; ApplyLineToContext(dialogLines[_currentLineIndex], _currentLineIndex, dialogLines.Count); _formController.OnDialogLineChanged(_formContext); return true; } public bool SkipDialog() { if (!_isPlaying) { Log.Warning("Skip dialog failed. No dialog is playing."); return false; } EndDialogInternal(); return true; } public void StopDialog() { _pendingStartDialogId = 0; if (_activeChapterTitleRequestId > 0) { _activeChapterTitleRequestId = 0; if (GameEntry.UI != null) { UGuiForm chapterTitleForm = GameEntry.UI.GetUIForm(UIFormId.ChapterTitleForm); if (chapterTitleForm != null) { GameEntry.UI.CloseUIForm(chapterTitleForm.UIForm); } } } if (!_isPlaying) { return; } EndDialogInternal(); } public void ClearRuntimeContext() { StopDialog(); _dialogMap.Clear(); _dialogLinesMap.Clear(); _dialogFirstLineIdMap.Clear(); _formContext = null; _currentChapterId = 0; _currentLineIndex = -1; _pendingStartDialogId = 0; _activeChapterTitleRequestId = 0; _chapterTitleShownAfterInit = false; _isInitialized = false; _isPlaying = false; } private bool EnsureDataTables() { _dtDialog = GameEntry.DataTable.GetDataTable(); if (_dtDialog == null) { Log.Warning("Dialog init failed. Data table DRDialog is missing."); return false; } _dtDialogLine = GameEntry.DataTable.GetDataTable(); if (_dtDialogLine == null) { Log.Warning("Dialog init failed. Data table DRDialogLine is missing."); return false; } return true; } private void EnsureFormController() { if (_formController == null) { _formController = new DialogFormController(); } } private bool TryGetCurrentDialogLines(out List dialogLines) { dialogLines = null; if (_formContext == null) { Log.Warning("Dialog state invalid. Form context is null."); return false; } if (!_dialogLinesMap.TryGetValue(_formContext.DialogId, out dialogLines)) { Log.Warning("Dialog state invalid. Dialog lines are missing for dialog '{0}'.", _formContext.DialogId.ToString()); return false; } return true; } private void EndDialogInternal() { int chapterId = _formContext != null ? _formContext.ChapterId : _currentChapterId; int dialogId = _formContext != null ? _formContext.DialogId : 0; int lineId = _formContext != null ? _formContext.CurrentLineId : 0; _isPlaying = false; _currentLineIndex = -1; _formController.OnDialogEnded(_formContext); _formController.CloseUI(() => { if (dialogId > 0) { GameEntry.Event.Fire(this, DialogCompletedEventArgs.Create(chapterId, dialogId, lineId)); } }); } private void ApplyLineToContext(DRDialogLine lineRow, int lineIndex, int totalLines) { _formContext.CurrentLineId = lineRow.Id; _formContext.SpeakerId = lineRow.SpeakerId; _formContext.SpeakerName = lineRow.SpeakerName; _formContext.Direction = lineRow.Direction; _formContext.Text = lineRow.Text; _formContext.Emphasis = lineRow.Emphasis; _formContext.LineIndex = lineIndex; _formContext.TotalLines = totalLines; _formContext.IsLastLine = lineIndex >= totalLines - 1; } private void PreloadChapterPortraitSprites() { if (GameEntry.SpriteCache == null) { return; } HashSet speakerIds = new HashSet(System.StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair> pair in _dialogLinesMap) { List lines = pair.Value; if (lines == null) { continue; } for (int i = 0; i < lines.Count; i++) { DRDialogLine line = lines[i]; if (line == null || !IsHumanSpeakerId(line.SpeakerId)) { continue; } speakerIds.Add(line.SpeakerId); } } if (speakerIds.Count == 0) { return; } foreach (string speakerId in speakerIds) { RegisterSpeakerPortraitsToCache(speakerId); } } private void RegisterSpeakerPortraitsToCache(string speakerId) { SpeakerPortraitBinding binding = FindSpeakerPortraitBinding(speakerId); if (binding == null || binding.Portraits == null || binding.Portraits.Count == 0) { Log.Warning("Portrait preload skipped. Missing speaker portrait binding. speakerId='{0}'.", speakerId); return; } int portraitIndex = 1; for (int i = 0; i < binding.Portraits.Count; i++) { Sprite portrait = binding.Portraits[i]; if (portrait == null) { continue; } string cacheKey = $"Characters/{speakerId}-{portraitIndex}.png"; GameEntry.SpriteCache.RegisterSprite(cacheKey, portrait, true); portraitIndex++; } } private SpeakerPortraitBinding FindSpeakerPortraitBinding(string speakerId) { if (string.IsNullOrWhiteSpace(speakerId) || _speakerPortraitBindings == null) { return null; } for (int i = 0; i < _speakerPortraitBindings.Count; i++) { SpeakerPortraitBinding binding = _speakerPortraitBindings[i]; if (binding == null || string.IsNullOrWhiteSpace(binding.SpeakerId)) { continue; } if (string.Equals(binding.SpeakerId.Trim(), speakerId.Trim(), StringComparison.OrdinalIgnoreCase)) { return binding; } } return null; } private static bool IsHumanSpeakerId(string speakerId) { if (string.IsNullOrWhiteSpace(speakerId)) { return false; } return !NonPortraitSpeakerIds.Contains(speakerId); } private bool TryOpenChapterTitle(DRDialog dialogRow) { if (GameEntry.UI == null) { return false; } int requestId = _nextChapterTitleRequestId++; string subtitle = dialogRow == null || string.IsNullOrWhiteSpace(dialogRow.Title) ? string.Empty : dialogRow.Title.Trim(); ChapterTitleFormContext context = new ChapterTitleFormContext { ChapterId = _currentChapterId, RequestId = requestId, Title = BuildChapterTitle(_currentChapterId), Subtitle = subtitle, DimFadeInDuration = _chapterTitleDimFadeInDuration, PreBlackDuration = _chapterTitlePreBlackDuration, TitleFadeInDelay = _chapterTitleTitleFadeInDelay, TitleFadeInDuration = _chapterTitleFadeInDuration, HoldDuration = _chapterTitleHoldDuration, TitleFadeOutDuration = _chapterTitleFadeOutDuration, DimFadeOutDuration = _chapterTitleDimFadeOutDuration }; int? formSerialId = GameEntry.UI.OpenUIForm(UIFormId.ChapterTitleForm, context); if (!formSerialId.HasValue) { Log.Warning("Open chapter title form failed. chapterId='{0}'.", _currentChapterId.ToString()); return false; } _activeChapterTitleRequestId = requestId; return true; } private static string BuildChapterTitle(int chapterId) { return chapterId > 0 ? $"第{chapterId}章" : string.Empty; } private static int ParseChapterIdFromDialogId(int dialogId) { return dialogId / DialogChapterDivisor; } private static int ParseChapterIdFromLineId(int lineId) { return lineId / LineChapterDivisor; } private static int ParseDialogIdFromLineId(int lineId) { return lineId / LineDialogDivisor; } #region Event Handlers private void OnDialogNextLineRequest(object sender, GameEventArgs e) { if (!(e is DialogNextLineRequestEventArgs)) { return; } NextLine(); } private void OnDialogSkipRequest(object sender, GameEventArgs e) { if (!(e is DialogSkipRequestEventArgs)) { return; } SkipDialog(); } private void OnDialogStopRequest(object sender, GameEventArgs e) { if (!(e is DialogStopRequestEventArgs)) { return; } StopDialog(); } private void OnChapterTitleCompleted(object sender, GameEventArgs e) { if (!(e is ChapterTitleCompletedEventArgs args)) { return; } if (args.RequestId <= 0 || args.RequestId != _activeChapterTitleRequestId) { return; } _activeChapterTitleRequestId = 0; int pendingDialogId = _pendingStartDialogId; _pendingStartDialogId = 0; if (pendingDialogId <= 0 || !_isInitialized) { return; } StartDialog(pendingDialogId); } #endregion } }