using System; using System.Collections.Generic; using System.Threading; using BriarQueen.Data.Identifiers; using BriarQueen.Framework.Coordinators.Events; using BriarQueen.Framework.Events.UI; using BriarQueen.Framework.Extensions; using BriarQueen.Framework.Managers.Interaction; using BriarQueen.Framework.Managers.IO; using BriarQueen.Framework.Managers.Player; using BriarQueen.Framework.Managers.UI.Base; using BriarQueen.Framework.Managers.UI.Events; using BriarQueen.Framework.Services.Settings; using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.UI; using VContainer; namespace BriarQueen.Framework.Managers.UI { /// /// UIManager: /// - Modal windows use the window stack /// - Non-modal UI (popups / fader) does not use the stack /// - HUD visibility is event-driven /// - Concrete UI implementations are hidden behind interfaces /// public class UIManager : IDisposable, IManager { private readonly EventCoordinator _eventCoordinator; private readonly InteractManager _interactManager; private readonly SaveManager _saveManager; private readonly SettingsService _settingsService; private readonly PlayerManager _playerManager; private readonly Dictionary _windows = new(); private readonly Stack _windowStack = new(); private readonly SemaphoreSlim _windowTransitionGate = new(1, 1); private bool _disposed; public bool Initialized { get; private set; } private sealed record OverlayResumeContext(WindowType OverlayWindowType, IUIOverlayHost Host); private IHud _hudContainer; private IPopup _infoPopup; private IPopup _tutorialPopup; private IScreenFader _screenFader; private IUIOverlayHost _mainMenuOverlayHost; private readonly Stack _overlayResumeStack = new(); [Inject] public UIManager( EventCoordinator eventCoordinator, InteractManager interactManager, SettingsService settingsService, SaveManager saveManager, PlayerManager playerManager) { _eventCoordinator = eventCoordinator; _interactManager = interactManager; _settingsService = settingsService; _saveManager = saveManager; _playerManager = playerManager; } private IUIWindow ActiveWindow => _windowStack.Count > 0 ? _windowStack.Peek() : null; public bool IsAnyUIOpen => _windowStack.Count > 0; public void Initialize() { if (Initialized) return; _disposed = false; SubscribeToEvents(); Initialized = true; } public void Dispose() { if (!Initialized || _disposed) return; _disposed = true; UnsubscribeFromEvents(); ResetUIStateHard(); Initialized = false; } private void SubscribeToEvents() { _eventCoordinator.Subscribe(OnPauseClickReceived); _eventCoordinator.Subscribe(OnBackRequested); _eventCoordinator.Subscribe(ToggleCodexWindow); _eventCoordinator.Subscribe(ToggleSettingsWindow); _eventCoordinator.Subscribe(OnFadeEvent); _eventCoordinator.Subscribe(OnDisplayInteractText); _eventCoordinator.Subscribe(OnTutorialDisplayPopup); _eventCoordinator.Subscribe(OnCodexChangedEvent); _eventCoordinator.Subscribe(OnHudToggleEvent); _eventCoordinator.Subscribe(OnToolbeltChangedEvent); } private void UnsubscribeFromEvents() { _eventCoordinator.Unsubscribe(OnPauseClickReceived); _eventCoordinator.Unsubscribe(OnBackRequested); _eventCoordinator.Unsubscribe(ToggleCodexWindow); _eventCoordinator.Unsubscribe(ToggleSettingsWindow); _eventCoordinator.Unsubscribe(OnFadeEvent); _eventCoordinator.Unsubscribe(OnDisplayInteractText); _eventCoordinator.Unsubscribe(OnTutorialDisplayPopup); _eventCoordinator.Unsubscribe(OnCodexChangedEvent); _eventCoordinator.Unsubscribe(OnHudToggleEvent); _eventCoordinator.Unsubscribe(OnToolbeltChangedEvent); } public void RegisterWindow(IUIWindow window) { if (window == null) return; _windows[window.WindowType] = window; window.Hide().Forget(); } public void UnregisterWindow(IUIWindow window) { if (window == null) return; if (_windows.TryGetValue(window.WindowType, out var registered) && ReferenceEquals(registered, window)) _windows.Remove(window.WindowType); if (window is IUIOverlayHost overlayHost) RemoveOverlayResumeContextsForHost(overlayHost); } public void RegisterHUD(IHud hudContainer) { _hudContainer = hudContainer; if (_hudContainer != null) { _hudContainer.Hide().Forget(); _interactManager.AddUIRaycaster(_hudContainer.Raycaster); } } public void RegisterInfoPopup(IPopup infoPopup) { _infoPopup = infoPopup; if (_infoPopup != null) _infoPopup.Hide().Forget(); } public void RegisterTutorialPopup(IPopup tutorialPopup) { _tutorialPopup = tutorialPopup; if (_tutorialPopup != null) _tutorialPopup.Hide().Forget(); } public void RegisterScreenFader(IScreenFader screenFader) { _screenFader = screenFader; } public void RegisterMainMenuOverlayHost(IUIOverlayHost host) { _mainMenuOverlayHost = host; } public void UnregisterMainMenuOverlayHost(IUIOverlayHost host) { if (!ReferenceEquals(_mainMenuOverlayHost, host)) return; RemoveOverlayResumeContextsForHost(host); _mainMenuOverlayHost = null; } private IUIWindow GetWindow(WindowType windowType) { return _windows.TryGetValue(windowType, out var window) ? window : null; } public bool IsWindowOpen(WindowType windowType) { var target = GetWindow(windowType); return target != null && _windowStack.Contains(target); } private void RemoveOverlayResumeContextsForHost(IUIOverlayHost host) { if (host == null || _overlayResumeStack.Count == 0) return; var contextsToKeep = new List(); foreach (var context in _overlayResumeStack) { if (!ReferenceEquals(context.Host, host)) contextsToKeep.Add(context); } _overlayResumeStack.Clear(); for (var i = contextsToKeep.Count - 1; i >= 0; i--) _overlayResumeStack.Push(contextsToKeep[i]); } private async UniTask TrySuspendActiveWindowFor(WindowType incomingWindowType) { if (ActiveWindow is IUIOverlayHost overlayHost && overlayHost.CanSuspendFor(incomingWindowType)) { await overlayHost.SuspendForOverlay(); _overlayResumeStack.Push(new OverlayResumeContext(incomingWindowType, overlayHost)); return true; } return false; } private async UniTask RestoreUnderlyingUi(WindowType closedWindowType) { if (_overlayResumeStack.Count > 0 && _overlayResumeStack.Peek().OverlayWindowType == closedWindowType) { var resumeContext = _overlayResumeStack.Pop(); await resumeContext.Host.ResumeFromOverlay(); return; } if (ActiveWindow != null) await ActiveWindow.Show(); } private async UniTask ApplyHudVisibility(bool visible) { if (_disposed || _hudContainer == null) return; try { if (visible) await _hudContainer.Show(); else await _hudContainer.Hide(); } catch (Exception ex) { Debug.LogError($"[UIManager] ApplyHudVisibility error: {ex}"); } } private void OnPauseClickReceived(PauseButtonClickedEvent _) { if (ActiveWindow == null) { OpenWindow(WindowType.PauseMenuWindow); return; } if (ActiveWindow.WindowType == WindowType.PauseMenuWindow) { TryHandleBackRequest(); return; } if (ActiveWindow.PauseBehavior == UIPauseBehavior.OpenPauseOverlay) { OpenWindow(WindowType.PauseMenuWindow); return; } TryHandleBackRequest(); } private void OnBackRequested(UIBackRequestedEvent _) { TryHandleBackRequest(); } private void ToggleSettingsWindow(UIToggleSettingsWindow eventData) { if (eventData.Show) OpenSettingsWindow(eventData.Source).Forget(); else CloseWindow(WindowType.SettingsWindow); } private void ToggleCodexWindow(ToggleCodexEvent eventData) { if(!_playerManager.CodexUnlocked()) return; if (eventData.Shown) { if (!IsWindowOpen(WindowType.CodexWindow)) OpenWindow(WindowType.CodexWindow); } else { if (IsWindowOpen(WindowType.CodexWindow)) CloseWindow(WindowType.CodexWindow); } } private void OnCodexChangedEvent(CodexChangedEvent eventData) { if (_infoPopup == null) return; var duration = _settingsService?.Game?.PopupDisplayDuration ?? 3f; var codexText = GetCodexTextForEntry(eventData.EntryType); _infoPopup.Play(codexText, duration).Forget(); } private void OnToolbeltChangedEvent(ToolbeltChangedEvent eventData) { if (_infoPopup == null) return; var duration = _settingsService?.Game?.PopupDisplayDuration ?? 3f; var toolText = GetToolbeltTextForEntry(eventData.ToolID, eventData.Lost); _infoPopup.Play(toolText, duration).Forget(); } private string GetToolbeltTextForEntry(ToolID toolID, bool lost) { if (lost) return $"You lost the {toolID.GetDisplayName()}."; else return $"You gained the {toolID.GetDisplayName()}."; } private string GetCodexTextForEntry(CodexType codexType) { return codexType switch { CodexType.DocumentEntry => "You've acquired a new document.", CodexType.PuzzleClue => "You've acquired a new puzzle clue.", CodexType.Photo => "You've acquired a new photo.", _ => string.Empty }; } private void OnTutorialDisplayPopup(DisplayTutorialPopupEvent eventData) { if (_tutorialPopup == null) return; if (!_settingsService.AreTutorialsEnabled()) return; if (string.IsNullOrWhiteSpace(eventData.ResolvedText)) { Debug.LogWarning($"[UIManager] Empty resolved text for tutorial '{eventData.TutorialID}'."); return; } var duration = _settingsService?.Game?.PopupDisplayDuration ?? 3f; _tutorialPopup.Play(eventData.ResolvedText, duration).Forget(); } private void OnDisplayInteractText(DisplayInteractEvent eventData) { if (_hudContainer == null) return; _hudContainer.DisplayInteractText(eventData.Message).Forget(); } private void OnFadeEvent(FadeEvent eventData) { if (_screenFader == null) return; if (eventData.Hidden) _screenFader.FadeFromAsync(eventData.Duration).Forget(); else _screenFader.FadeToAsync(eventData.Duration).Forget(); } private void OnHudToggleEvent(UIToggleHudEvent eventData) { ApplyHudVisibility(eventData.Show).Forget(); } public void OpenWindow(WindowType windowType) { OpenWindowInternal(windowType).Forget(); } private async UniTask OpenSettingsWindow(SettingsOpenSource source) { await _windowTransitionGate.WaitAsync(); try { if (_disposed) return; var window = GetWindow(WindowType.SettingsWindow); if (window == null) { Debug.LogError("[UIManager] Window of type SettingsWindow not registered."); return; } if (_windowStack.Contains(window)) return; var suspended = false; if (source == SettingsOpenSource.MainMenu && _mainMenuOverlayHost != null && _mainMenuOverlayHost.CanSuspendFor(WindowType.SettingsWindow)) { await _mainMenuOverlayHost.SuspendForOverlay(); _overlayResumeStack.Push(new OverlayResumeContext(WindowType.SettingsWindow, _mainMenuOverlayHost)); suspended = true; } else { suspended = await TrySuspendActiveWindowFor(WindowType.SettingsWindow); } if (!suspended && ActiveWindow != null) { await ActiveWindow.Hide(); } _windowStack.Push(window); await window.Show(); NotifyWindowStateChanged(window.WindowType, true); NotifyUIStackChanged(); } finally { _windowTransitionGate.Release(); } } private async UniTask OpenWindowInternal(WindowType windowType) { await _windowTransitionGate.WaitAsync(); try { if (_disposed) return; var window = GetWindow(windowType); if (window == null) { Debug.LogError($"[UIManager] Window of type {windowType} not registered."); return; } if (_windowStack.Contains(window)) return; var suspended = await TrySuspendActiveWindowFor(windowType); if (!suspended && ActiveWindow != null) await ActiveWindow.Hide(); _windowStack.Push(window); await window.Show(); NotifyWindowStateChanged(window.WindowType, true); NotifyUIStackChanged(); } finally { _windowTransitionGate.Release(); } } public void CloseWindow(WindowType windowType) { CloseWindowInternal(windowType).Forget(); } private async UniTask CloseWindowInternal(WindowType windowType) { await _windowTransitionGate.WaitAsync(); try { if (_disposed || _windowStack.Count == 0) return; var target = GetWindow(windowType); if (target == null || !_windowStack.Contains(target)) return; while (_windowStack.Count > 0) { var current = _windowStack.Pop(); if (current != null) { await current.Hide(); NotifyWindowStateChanged(current.WindowType, false); } if (current == target) break; } await RestoreUnderlyingUi(target.WindowType); NotifyUIStackChanged(); } finally { _windowTransitionGate.Release(); } } public void CloseTopWindow() { CloseTopWindowInternal().Forget(); } private void TryHandleBackRequest() { if (_disposed || _windowStack.Count == 0) { return; } if (ActiveWindow is IUIBackHandler backHandler && backHandler.HandleBackRequest()) { return; } CloseTopWindow(); } private async UniTask CloseTopWindowInternal() { await _windowTransitionGate.WaitAsync(); try { if (_disposed || _windowStack.Count == 0) return; var top = _windowStack.Pop(); if (top != null) { await top.Hide(); NotifyWindowStateChanged(top.WindowType, false); } if (top != null) await RestoreUnderlyingUi(top.WindowType); NotifyUIStackChanged(); } finally { _windowTransitionGate.Release(); } } public void ResetUIState() { ResetUIStateAsync().Forget(); } public async UniTask ResetUIStateAsync() { await _windowTransitionGate.WaitAsync(); try { while (_windowStack.Count > 0) { var window = _windowStack.Pop(); if (window == null) continue; try { await window.Hide(); NotifyWindowStateChanged(window.WindowType, false); } catch { } } _overlayResumeStack.Clear(); if (_tutorialPopup != null) { try { await _tutorialPopup.Hide(); } catch { } } if (_infoPopup != null) { try { await _infoPopup.Hide(); } catch { } } NotifyUIStackChanged(); } finally { _windowTransitionGate.Release(); } } private void ResetUIStateHard() { foreach (var kv in _windows) { if (kv.Value is Component component && component != null) component.gameObject.SetActive(false); } if (_tutorialPopup?.GameObject != null) _tutorialPopup.GameObject.SetActive(false); if (_infoPopup?.GameObject != null) _infoPopup.GameObject.SetActive(false); if (_hudContainer is Component hudComponent && hudComponent != null) hudComponent.gameObject.SetActive(false); if (_screenFader is Component faderComponent && faderComponent != null) faderComponent.gameObject.SetActive(false); _windowStack.Clear(); _overlayResumeStack.Clear(); _mainMenuOverlayHost = null; } private void NotifyUIStackChanged() { _eventCoordinator.Publish(new UIStackChangedEvent(_windowStack.Count > 0)); } private void NotifyWindowStateChanged(WindowType windowType, bool isOpen) { _eventCoordinator.Publish(new UIWindowStateChangedEvent(windowType, isOpen)); } } }