using System.Numerics; using AcDream.App.Input; using AcDream.App.Net; using AcDream.UI.Abstractions; using AcDream.UI.Abstractions.Panels.Chat; using AcDream.UI.Abstractions.Panels.Debug; using AcDream.UI.Abstractions.Panels.Settings; using AcDream.UI.Abstractions.Panels.Vitals; using AcDream.UI.ImGui; using ImGuiNET; namespace AcDream.App.Rendering; internal enum DevToolsPanelLayoutCondition { FirstUseEver, Always, } internal enum DevToolsPanelKind { Vitals, Chat, Debug, Settings, } internal interface IDevToolsFrameLifecycle : IRenderFrameFailureRecovery { void BeginFrame(float deltaSeconds); void Render(double deltaSeconds, int viewportWidth, int viewportHeight); } internal interface IDevToolsFrameBackend : IDisposable { void BeginFrame(float deltaSeconds); void AbortFrame(); bool BeginMainMenuBar(); void EndMainMenuBar(); bool BeginMenu(string label); void EndMenu(); bool MenuItem(string label, string? shortcut = null, bool selected = false); void Separator(); void RenderPanels(PanelContext context); void RenderDrawData(); void SetWindowLayout( string title, Vector2 position, Vector2 size, DevToolsPanelLayoutCondition condition); } internal interface IDevToolsCameraMenuOperations { bool IsFlyMode { get; } bool CollideCamera { get; set; } void ToggleFlyOrChase(); } internal interface IDevToolsCommandBusSource { ICommandBus Current { get; } } internal interface IDevToolsPanelSet { bool Contains(DevToolsPanelKind kind); string? GetTitle(DevToolsPanelKind kind); bool IsVisible(DevToolsPanelKind kind); void Toggle(DevToolsPanelKind kind); void FocusChatInput(); void AddDebugToast(string message); } /// Concrete ImGui backend for the developer presentation owner. internal sealed class ImGuiDevToolsFrameBackend : IDevToolsFrameBackend, IDisposable { private readonly IImGuiBootstrapper _bootstrap; private readonly ImGuiPanelHost _panels; private bool _disposed; public ImGuiDevToolsFrameBackend( IImGuiBootstrapper bootstrap, ImGuiPanelHost panels) { _bootstrap = bootstrap ?? throw new ArgumentNullException(nameof(bootstrap)); _panels = panels ?? throw new ArgumentNullException(nameof(panels)); } public void BeginFrame(float deltaSeconds) => _bootstrap.BeginFrame(deltaSeconds); public void AbortFrame() => _bootstrap.AbortFrame(); public bool BeginMainMenuBar() => ImGuiNET.ImGui.BeginMainMenuBar(); public void EndMainMenuBar() => ImGuiNET.ImGui.EndMainMenuBar(); public bool BeginMenu(string label) => ImGuiNET.ImGui.BeginMenu(label); public void EndMenu() => ImGuiNET.ImGui.EndMenu(); public bool MenuItem(string label, string? shortcut = null, bool selected = false) => ImGuiNET.ImGui.MenuItem(label, shortcut ?? string.Empty, selected); public void Separator() => ImGuiNET.ImGui.Separator(); public void RenderPanels(PanelContext context) => _panels.RenderAll(context); public void RenderDrawData() => _bootstrap.Render(); public void SetWindowLayout( string title, Vector2 position, Vector2 size, DevToolsPanelLayoutCondition condition) { ImGuiCond imguiCondition = condition switch { DevToolsPanelLayoutCondition.FirstUseEver => ImGuiCond.FirstUseEver, DevToolsPanelLayoutCondition.Always => ImGuiCond.Always, _ => throw new ArgumentOutOfRangeException(nameof(condition)), }; ImGuiNET.ImGui.SetWindowPos(title, position, imguiCondition); ImGuiNET.ImGui.SetWindowSize(title, size, imguiCondition); } public void Dispose() { if (_disposed) return; _bootstrap.Dispose(); _disposed = true; } } /// Mutable late-composition seam for player-mode menu operations. internal sealed class DevToolsCameraMenuOperations : IDevToolsCameraMenuOperations { private readonly CameraController _camera; private readonly IDevToolsPlayerModeCommands _playerMode; public DevToolsCameraMenuOperations( CameraController camera, IDevToolsPlayerModeCommands playerMode) { _camera = camera ?? throw new ArgumentNullException(nameof(camera)); _playerMode = playerMode ?? throw new ArgumentNullException(nameof(playerMode)); } public bool IsFlyMode => _camera.IsFlyMode; public bool CollideCamera { get => AcDream.Core.Rendering.CameraDiagnostics.CollideCamera; set => AcDream.Core.Rendering.CameraDiagnostics.CollideCamera = value; } public void ToggleFlyOrChase() => _playerMode.ToggleFlyOrChase(); } /// Resolves the reconnect-safe command bus at draw time. internal sealed class DevToolsCommandBusSource : IDevToolsCommandBusSource { private ILiveUiSessionTarget? _session; private bool _deactivated; public ICommandBus Current => !_deactivated && _session is { } session ? session.Commands : NullCommandBus.Instance; public void Bind(ILiveUiSessionTarget session) { ArgumentNullException.ThrowIfNull(session); ObjectDisposedException.ThrowIf(_deactivated, this); if (_session is not null && !ReferenceEquals(_session, session)) throw new InvalidOperationException( "Developer command-bus authority is already bound."); _session = session; } public IDisposable BindOwned(ILiveUiSessionTarget session) { ArgumentNullException.ThrowIfNull(session); ObjectDisposedException.ThrowIf(_deactivated, this); if (_session is not null) { throw new InvalidOperationException( "Developer command-bus authority is already bound."); } _session = session; return new Binding(this, session); } public void Unbind(ILiveUiSessionTarget session) { ArgumentNullException.ThrowIfNull(session); if (ReferenceEquals(_session, session)) _session = null; } public void Deactivate() { _deactivated = true; _session = null; } private sealed class Binding : IDisposable { private DevToolsCommandBusSource? _owner; private readonly ILiveUiSessionTarget _expected; public Binding( DevToolsCommandBusSource owner, ILiveUiSessionTarget expected) { _owner = owner; _expected = expected; } public void Dispose() => Interlocked.Exchange(ref _owner, null)?.Unbind(_expected); } } /// Typed panel operations used by menu and input presentation. internal sealed class DevToolsPanelSet : IDevToolsPanelSet { private readonly VitalsPanel _vitals; private readonly ChatPanel _chat; private readonly DebugPanel _debug; private readonly DebugVM _debugViewModel; private readonly SettingsPanel? _settings; public DevToolsPanelSet( VitalsPanel vitals, ChatPanel chat, DebugPanel debug, DebugVM debugViewModel, SettingsPanel? settings) { _vitals = vitals ?? throw new ArgumentNullException(nameof(vitals)); _chat = chat ?? throw new ArgumentNullException(nameof(chat)); _debug = debug ?? throw new ArgumentNullException(nameof(debug)); _debugViewModel = debugViewModel ?? throw new ArgumentNullException(nameof(debugViewModel)); _settings = settings; } public bool Contains(DevToolsPanelKind kind) => kind != DevToolsPanelKind.Settings || _settings is not null; public string? GetTitle(DevToolsPanelKind kind) => GetPanel(kind)?.Title; public bool IsVisible(DevToolsPanelKind kind) => GetPanel(kind)?.IsVisible == true; public void Toggle(DevToolsPanelKind kind) { if (GetPanel(kind) is { } panel) panel.IsVisible = !panel.IsVisible; } public void FocusChatInput() => _chat.FocusInput(); public void AddDebugToast(string message) => _debugViewModel.AddToast(message); private IPanel? GetPanel(DevToolsPanelKind kind) => kind switch { DevToolsPanelKind.Vitals => _vitals, DevToolsPanelKind.Chat => _chat, DevToolsPanelKind.Debug => _debug, DevToolsPanelKind.Settings => _settings, _ => throw new ArgumentOutOfRangeException(nameof(kind)), }; } /// /// Owns the optional ImGui developer frame, menu policy, panel actions, and /// reusable default layout. Retained gameplay UI remains a separate earlier /// presentation phase. /// internal sealed class DevToolsFramePresenter : IDevToolsFrameLifecycle { private readonly IDevToolsFrameBackend _backend; private readonly IDevToolsCameraMenuOperations _camera; private readonly IDevToolsCommandBusSource _commands; private readonly IDevToolsPanelSet _panels; private readonly AcDream.App.Diagnostics.FrameProfiler _profiler; private bool _frameOpen; public DevToolsFramePresenter( IDevToolsFrameBackend backend, IDevToolsCameraMenuOperations camera, IDevToolsCommandBusSource commands, AcDream.App.Diagnostics.FrameProfiler profiler, IDevToolsPanelSet panels) { _backend = backend ?? throw new ArgumentNullException(nameof(backend)); _camera = camera ?? throw new ArgumentNullException(nameof(camera)); _commands = commands ?? throw new ArgumentNullException(nameof(commands)); _profiler = profiler ?? throw new ArgumentNullException(nameof(profiler)); _panels = panels ?? throw new ArgumentNullException(nameof(panels)); } public void BeginFrame(float deltaSeconds) { if (_frameOpen) { throw new InvalidOperationException( "The previous developer-tools frame must render or abort before a new frame begins."); } _frameOpen = true; _backend.BeginFrame(deltaSeconds); } public void Render(double deltaSeconds, int viewportWidth, int viewportHeight) { if (!_frameOpen) { throw new InvalidOperationException( "BeginFrame must open the developer-tools frame before Render."); } var context = new PanelContext((float)deltaSeconds, _commands.Current); DrawMenu(viewportWidth, viewportHeight); _backend.RenderPanels(context); using var stage = _profiler.BeginStage(AcDream.App.Diagnostics.FrameStage.ImGui); _backend.RenderDrawData(); _frameOpen = false; } public void AbortFrame() { if (!_frameOpen) return; try { _backend.AbortFrame(); } finally { _frameOpen = false; } } public void ToggleDebugPanel() { _panels.Toggle(DevToolsPanelKind.Debug); _panels.AddDebugToast( $"Debug panel {(_panels.IsVisible(DevToolsPanelKind.Debug) ? "ON" : "OFF")}"); } public void FocusChatInput() => _panels.FocusChatInput(); public void ToggleSettingsPanel() { _panels.Toggle(DevToolsPanelKind.Settings); } public void ResetLayout( int viewportWidth, int viewportHeight, DevToolsPanelLayoutCondition condition) { float width = Math.Max(viewportWidth, 480); float height = Math.Max(viewportHeight, 320); SetLayout( DevToolsPanelKind.Vitals, new Vector2(10f, 30f), new Vector2(220f, 110f), condition); SetLayout( DevToolsPanelKind.Chat, new Vector2(10f, height - 320f), new Vector2(450f, 300f), condition); SetLayout( DevToolsPanelKind.Debug, new Vector2(width - 380f, 30f), new Vector2(370f, 520f), condition); if (_panels.Contains(DevToolsPanelKind.Settings)) { SetLayout( DevToolsPanelKind.Settings, new Vector2((width - 700f) * 0.5f, (height - 500f) * 0.5f), new Vector2(700f, 500f), condition); } } private void DrawMenu(int viewportWidth, int viewportHeight) { if (!_backend.BeginMainMenuBar()) return; try { if (_backend.BeginMenu("View")) { try { if (_panels.Contains(DevToolsPanelKind.Settings) && _backend.MenuItem("Settings", "F11")) { _panels.Toggle(DevToolsPanelKind.Settings); } if (_backend.MenuItem("Vitals")) _panels.Toggle(DevToolsPanelKind.Vitals); if (_backend.MenuItem("Chat")) _panels.Toggle(DevToolsPanelKind.Chat); if (_backend.MenuItem("Debug", "Ctrl+F1")) _panels.Toggle(DevToolsPanelKind.Debug); _backend.Separator(); if (_backend.MenuItem("Reset window layout")) ResetLayout( viewportWidth, viewportHeight, DevToolsPanelLayoutCondition.Always); } finally { _backend.EndMenu(); } } if (_backend.BeginMenu("Camera")) { try { string flyLabel = _camera.IsFlyMode ? "Exit Free-Fly Mode" : "Enter Free-Fly Mode"; if (_backend.MenuItem(flyLabel, "Ctrl+Shift+F")) _camera.ToggleFlyOrChase(); bool collide = _camera.CollideCamera; if (_backend.MenuItem( "Collide Camera (spring arm)", selected: collide)) { _camera.CollideCamera = !collide; } } finally { _backend.EndMenu(); } } } finally { _backend.EndMainMenuBar(); } } private void SetLayout( DevToolsPanelKind panel, Vector2 position, Vector2 size, DevToolsPanelLayoutCondition condition) { string? title = _panels.GetTitle(panel); if (!string.IsNullOrEmpty(title)) _backend.SetWindowLayout(title, position, size, condition); } }