using System; using System.Collections; using System.Collections.Generic; using AibisDream.FixSystem; using AibisDream.Framework; using AibisDream.Kit; using AibisDream.SaveSystem; using AibisDream.UI; using UnityEngine; using UnityEngine.EventSystems; namespace AibisDream { public sealed class SessionFailure { public string Command { get; internal set; } public string Stage { get; internal set; } public IReadOnlyList Errors { get; internal set; } = Array.Empty(); } public class GameManager : Singleton { private enum SessionCommandKind { None, Start, Transition, Restore, ReturnToMenu } private const float TransitionDuration = 0.5f; private const float DialogueHandoffTimeout = 5f; [Header("生产环境数据(一般不动)")] [SerializeField] private TalkSceneSO firstTalkSo; private GameSession _session; private TalkSceneGraphIndex _talkSceneIndex; private SessionCommandKind _activeCommand; private bool _returnToMenuRequested; public static GameSession Session => Instance != null && Instance._session != null ? Instance._session : throw new InvalidOperationException( "GameManager.Session accessed before GameManager initialization."); public IReadOnlyList RuntimeChapters => _talkSceneIndex.RuntimeChapters; public override void OnSingletonInit() { _session = new GameSession(); _talkSceneIndex = new TalkSceneGraphIndex(firstTalkSo); foreach (var error in _talkSceneIndex.ValidationErrors) { Debug.LogError($"[GameManager] {error}"); } } private void Start() { if (DialogController.Instance != null) { DialogController.Instance.DialogueActivityChanged += OnDialogueActivityChanged; } } public override void OnSingletonDestroy() { if (DialogController.Instance != null) { DialogController.Instance.DialogueActivityChanged -= OnDialogueActivityChanged; } } public bool TryStartNewGame() { return TryStartChapter(firstTalkSo); } public bool TryStartChapter(TalkSceneSO chapter) { if (!ValidateChapter(chapter, out var error)) { Debug.LogError($"[GameManager] Cannot start chapter: {error}"); return false; } if (!TryAcquireCommand(SessionCommandKind.Start)) { return false; } StartCoroutine(StartChapterRoutine(chapter)); return true; } public bool TryRestoreSlot(int slotIndex, Action completed = null) { if (!CanBeginRestore() || !TryAcquireCommand(SessionCommandKind.Restore)) { return false; } StartCoroutine(RestoreRoutine( () => SaveRestoreOrchestrator.PrepareRestoreFromSlot( slotIndex, _talkSceneIndex, RestoreOptions.Default), completed)); return true; } public bool TryRestoreFile( string path, RestoreOptions options, Action completed = null) { if (!CanBeginRestore() || !TryAcquireCommand(SessionCommandKind.Restore)) { return false; } StartCoroutine(RestoreRoutine( () => SaveRestoreOrchestrator.PrepareRestoreFromFile( path, _talkSceneIndex, options ?? RestoreOptions.Default), completed)); return true; } public bool TryPause() { if (!_session.CanPause) { return false; } _session.SetPaused(true); Time.timeScale = 0f; EnumEventSystem.Global.Send(GameLifecycleEvent.SessionPaused); return true; } public bool TryResume() { if (!_session.CanResume) { return false; } ResumeSessionInternal(sendEvent: true); return true; } public bool TryReturnToMainMenu() { if (_session.Phase is GameSessionPhase.MainMenu or GameSessionPhase.ReturningToMenu) { return false; } if (_activeCommand != SessionCommandKind.None) { _returnToMenuRequested = true; return true; } if (!TryAcquireCommand(SessionCommandKind.ReturnToMenu)) { return false; } StartCoroutine(ReturnToMainMenuRoutine()); return true; } public void QuitApplication() { Application.Quit(); } internal bool TryAdvanceChapterFromYarn(string exitName) { var target = _session.CurrentTalkScene?.GetNextScene(exitName); if (!ValidateChapter(target, out var error)) { Debug.LogError($"[GameManager] Invalid chapter exit '{exitName ?? "default"}': {error}"); return false; } if (_session.Phase != GameSessionPhase.Playing || !TryAcquireCommand(SessionCommandKind.Transition)) { Debug.LogWarning("[GameManager] Chapter transition rejected because another command is active."); return false; } StartCoroutine(AdvanceChapterCommandRoutine(target)); return true; } private IEnumerator AdvanceChapterCommandRoutine(TalkSceneSO target) { try { yield return TransitionChapterRoutine(target); } finally { FinishCommand(SessionCommandKind.Transition); } } internal IEnumerator LoadSceneFromYarn(string sceneName) { if (_session.Phase != GameSessionPhase.Playing || !TryAcquireCommand(SessionCommandKind.Transition)) { Debug.LogWarning("[GameManager] Yarn scene load rejected because another command is active."); yield break; } try { DialogUIManager.Instance?.HideDialog(); if (!TrySetPhase(GameSessionPhase.Transitioning)) { yield break; } SceneOperationResult sceneResult = null; yield return SceneLoader.Instance.LoadSceneAsync(sceneName, value => sceneResult = value); if (!EnsureSceneSucceeded(sceneResult, "Yarn scene load")) { yield break; } SceneReadinessResult readiness = null; yield return SceneReadiness.WaitUntilReady( SceneLoader.Instance.CurrentScene, IsReturnRequested, value => readiness = value); if (!EnsureReadinessSucceeded(readiness, "Yarn scene readiness")) { yield break; } if (_returnToMenuRequested) { yield break; } TrySetPhase(GameSessionPhase.Playing); EnumEventSystem.Global.Send(GameLifecycleEvent.SessionReady); } finally { FinishCommand(SessionCommandKind.Transition); } } internal IEnumerator UnloadSceneFromYarn() { if (_session.Phase != GameSessionPhase.Playing || !TryAcquireCommand(SessionCommandKind.Transition)) { Debug.LogWarning("[GameManager] Yarn scene unload rejected because another command is active."); yield break; } try { if (!TrySetPhase(GameSessionPhase.Transitioning)) { yield break; } SceneOperationResult result = null; yield return SceneLoader.Instance.UnloadSceneAsync(value => result = value); if (!EnsureSceneSucceeded(result, "Yarn scene unload")) { yield break; } if (_returnToMenuRequested) { yield break; } TrySetPhase(GameSessionPhase.Playing); EnumEventSystem.Global.Send(GameLifecycleEvent.SessionReady); } finally { FinishCommand(SessionCommandKind.Transition); } } private IEnumerator StartChapterRoutine(TalkSceneSO chapter) { try { // 设置当前状态 _session.ResetRuntimeState(); if (!TrySetPhase(GameSessionPhase.Starting)) { yield break; } EnumEventSystem.Global.Send(GameLifecycleEvent.SessionStarted); yield return FadeIn(); if (_returnToMenuRequested) { yield break; } SceneOperationResult sceneResult = null; yield return SceneLoader.Instance.LoadSceneAsync( chapter.firstSceneName, value => sceneResult = value); if (!EnsureSceneSucceeded(sceneResult, "Start scene")) { yield break; } SceneReadinessResult readiness = null; yield return SceneReadiness.WaitUntilReady( SceneLoader.Instance.CurrentScene, IsReturnRequested, value => readiness = value); if (!EnsureReadinessSucceeded(readiness, "Start readiness")) { yield break; } if (_returnToMenuRequested) { yield break; } CommitChapter(chapter); DialogController.Instance.StartDialog(chapter.yarnProject); TrySetPhase(GameSessionPhase.Playing); EnumEventSystem.Global.Send(GameLifecycleEvent.SessionReady); // 让Yarn自己处理淡出 // yield return FadeOut(); } finally { FinishCommand(SessionCommandKind.Start); } } private IEnumerator TransitionChapterRoutine(TalkSceneSO target) { if (!TrySetPhase(GameSessionPhase.Transitioning)) { yield break; } yield return FadeIn(); if (_returnToMenuRequested) { yield break; } var sourceDialogueCompleted = false; yield return WaitForSourceDialogueCompletion( value => sourceDialogueCompleted = value); if (_returnToMenuRequested) { yield break; } if (!sourceDialogueCompleted) { SendFailure( "Transition", "SourceDialogueCompletion", new[] { $"Source dialogue did not complete within {DialogueHandoffTimeout:0.#} seconds. " + "NextYarn must be the final executable command on its branch." }); _returnToMenuRequested = true; yield break; } SceneOperationResult sceneResult = null; yield return SceneLoader.Instance.LoadSceneAsync( target.firstSceneName, value => sceneResult = value); if (!EnsureSceneSucceeded(sceneResult, "Chapter scene")) { yield break; } SceneReadinessResult readiness = null; yield return SceneReadiness.WaitUntilReady( SceneLoader.Instance.CurrentScene, IsReturnRequested, value => readiness = value); if (!EnsureReadinessSucceeded(readiness, "Chapter readiness")) { yield break; } if (_returnToMenuRequested) { yield break; } CommitChapter(target); DialogController.Instance.StartDialog(target.yarnProject); TrySetPhase(GameSessionPhase.Playing); EnumEventSystem.Global.Send(GameLifecycleEvent.SessionReady); // 让Yarn自己处理淡出 // yield return FadeOut(); } private IEnumerator WaitForSourceDialogueCompletion(Action completed) { var runner = DialogController.Instance?.DialogueRunner; if (runner == null) { completed?.Invoke(false); yield break; } var deadline = Time.unscaledTime + DialogueHandoffTimeout; while (runner.IsDialogueRunning && Time.unscaledTime < deadline) { if (_returnToMenuRequested) { completed?.Invoke(false); yield break; } yield return null; } completed?.Invoke(!runner.IsDialogueRunning); } private IEnumerator RestoreRoutine( Func prepare, Action completed) { var originPhase = _session.Phase; RestoreResult result = null; try { if (!TrySetPhase(GameSessionPhase.Restoring)) { yield break; } var preparation = prepare(); result = preparation.Result; if (!preparation.Success) { SendFailure("Restore", result.FailedPhase, result.Errors); TrySetPhase(originPhase); yield break; } EnumEventSystem.Global.Send(GameLifecycleEvent.SessionStarted); ResumeSessionInternal(sendEvent: _session.IsPaused); yield return FadeIn(); if (_returnToMenuRequested) { result.Cancelled = true; yield break; } if (DialogController.Instance != null) { yield return DialogController.Instance.StopDialogRoutine(); } if (_returnToMenuRequested) { result.Cancelled = true; yield break; } ResetPresentationEffects(); yield return SaveRestoreOrchestrator.ExecutePreparedRestore( preparation, IsReturnRequested); result = preparation.Result; if (result.Cancelled || _returnToMenuRequested) { _returnToMenuRequested = true; yield break; } if (!result.Success) { SendFailure("Restore", result.FailedPhase, result.Errors); _returnToMenuRequested = true; yield break; } CommitChapter(result.RestoredTalkScene); TrySetPhase(GameSessionPhase.Playing); EnumEventSystem.Global.Send(GameLifecycleEvent.SessionReady); ScreenSnapshotHelper.ApplyDeferredFadeScreenIfNeeded(); // yield return FadeOut(); } finally { completed?.Invoke(result ?? RestoreResult.CreateFailure( "Restore", "Restore did not produce a result.")); FinishCommand(SessionCommandKind.Restore); } } private IEnumerator ReturnToMainMenuRoutine() { try { _returnToMenuRequested = false; if (!TrySetPhase(GameSessionPhase.ReturningToMenu)) { yield break; } EnumEventSystem.Global.Send(GameLifecycleEvent.SessionEnding); ResumeSessionInternal(sendEvent: _session.IsPaused); yield return FadeIn(); if (DialogController.Instance != null) { yield return DialogController.Instance.StopDialogRoutine(); } // The showcase belongs to the loaded gameplay scene, so clear it // while that scene is still alive and hidden behind the fade. ResetPresentationEffects(); SceneOperationResult unloadResult = null; const int maxUnloadAttempts = 2; for (var attempt = 1; attempt <= maxUnloadAttempts; attempt++) { yield return SceneLoader.Instance.UnloadSceneAsync(value => unloadResult = value); if (unloadResult?.Success == true) { break; } if (attempt < maxUnloadAttempts) { yield return null; } } if (unloadResult?.Success != true) { SendFailure( "ReturnToMenu", "UnloadScene", new[] { unloadResult?.Error ?? "Scene unload did not return a result." }); yield break; } UIManager.Instance.HidePanel(); yield return LocalizationKit.WaitUntilReady(); _session.ResetRuntimeState(); TrySetPhase(GameSessionPhase.MainMenu); EnumEventSystem.Global.Send(GameLifecycleEvent.SessionEnded); yield return LocalizationKit.WaitForUIRefresh(); yield return FadeOut(); EventSystem.current?.SetSelectedGameObject(null); } finally { FinishCommand(SessionCommandKind.ReturnToMenu); } } private bool CanBeginRestore() { return _activeCommand == SessionCommandKind.None && _session.Phase is GameSessionPhase.MainMenu or GameSessionPhase.Playing; } private bool TryAcquireCommand(SessionCommandKind command) { if (_activeCommand != SessionCommandKind.None) { return false; } _activeCommand = command; return true; } private void FinishCommand(SessionCommandKind command) { if (_activeCommand == command) { _activeCommand = SessionCommandKind.None; } if (!_returnToMenuRequested || _activeCommand != SessionCommandKind.None || _session.Phase is GameSessionPhase.MainMenu or GameSessionPhase.ReturningToMenu) { return; } if (TryAcquireCommand(SessionCommandKind.ReturnToMenu)) { StartCoroutine(ReturnToMainMenuRoutine()); } } private bool TrySetPhase(GameSessionPhase phase) { if (_session.TryTransitionTo(phase, out var error)) { return true; } Debug.LogError($"[GameManager] {error}"); return false; } private void CommitChapter(TalkSceneSO chapter) { _session.SetCurrentTalkScene(chapter); ChapterController.Instance?.UnlockChapter(chapter); } private bool EnsureSceneSucceeded(SceneOperationResult result, string stage) { if (result?.Success == true) { return true; } SendFailure("Scene", stage, new[] { result?.Error ?? "Scene operation did not return a result." }); _returnToMenuRequested = true; return false; } private bool EnsureReadinessSucceeded(SceneReadinessResult result, string stage) { if (result?.Success == true) { return true; } if (result?.Cancelled == true) { _returnToMenuRequested = true; return false; } SendFailure("SceneReadiness", stage, new[] { result?.Error ?? "Scene readiness did not return a result." }); _returnToMenuRequested = true; return false; } private void SendFailure(string command, string stage, IReadOnlyList errors) { var failure = new SessionFailure { Command = command, Stage = stage, Errors = errors ?? Array.Empty() }; Debug.LogError($"[GameManager] {command} failed at {stage}: {string.Join("; ", failure.Errors)}"); EnumEventSystem.Global.Send( GameLifecycleEvent.SessionFailed, failure); } private void ResumeSessionInternal(bool sendEvent) { var wasPaused = _session.IsPaused; Time.timeScale = 1f; _session.SetPaused(false); if (sendEvent && wasPaused) { EnumEventSystem.Global.Send(GameLifecycleEvent.SessionResumed); } } private void ResetPresentationEffects() { ScreenEffectManager.Instance?.ResetSaturation(); UIManager.Instance.GetPanel()?.ClearPresentationEffects(); Day2SleepPresentationController.Instance?.ClearPresentationEffects(); SpriteShowcase.Instance?.ClearPresentationEffects(); CameraKit.Instance?.ResetCamera(); } private IEnumerator FadeIn() { var transitionPanel = UIManager.Instance.GetPanel(); if (transitionPanel != null) { yield return transitionPanel.FadeInAsync(TransitionDuration, useUnscaledTime: true); } } private IEnumerator FadeOut() { var transitionPanel = UIManager.Instance.GetPanel(); if (transitionPanel != null) { yield return transitionPanel.FadeOutAsync(TransitionDuration, useUnscaledTime: true); } } private void OnDialogueActivityChanged(bool active) { _session.SetDialogueActive(active); } private bool IsReturnRequested() { return _returnToMenuRequested; } private static bool ValidateChapter(TalkSceneSO chapter, out string error) { if (chapter == null) { error = "TalkSceneSO is null."; return false; } if (string.IsNullOrWhiteSpace(chapter.firstSceneName)) { error = $"TalkSceneSO {chapter.name} has an empty scene key."; return false; } if (chapter.yarnProject == null) { error = $"TalkSceneSO {chapter.name} has no YarnProject."; return false; } error = null; return true; } } }