using AcDream.App.Combat; using AcDream.App.Diagnostics; using AcDream.App.Input; using AcDream.App.Rendering; using AcDream.App.Settings; using AcDream.Core.Chat; using AcDream.Core.Combat; using AcDream.Core.Player; using AcDream.Runtime; using AcDream.Runtime.Gameplay; using AcDream.UI.Abstractions.Input; 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 Silk.NET.Input; using Silk.NET.OpenGL; using Silk.NET.Windowing; namespace AcDream.App.Composition; internal sealed record SettingsDevToolsResult( AcDream.UI.Abstractions.Settings.QualitySettings ResolvedQuality, DevToolsCompositionOwner? DevTools); internal sealed record SettingsDevToolsOptionalDependencies( IDevToolsRuntimeFacts Facts, IRuntimeKeyBindingTarget? KeyBindingTarget, DeferredCanonicalWorldEntityCountSource WorldEntities, DeferredRenderFrameDiagnosticsSource FrameDiagnostics, DeferredDevToolsPlayerModeCommands PlayerModeCommands); internal sealed record SettingsDevToolsDependencies( IView Window, RuntimeSettingsController Settings, IRuntimeSettingsStartupTarget StartupTarget, HostQuiescenceGate HostQuiescence, GameRuntime Runtime, SettingsDevToolsOptionalDependencies? DevTools, RuntimeDiagnosticCommandSlot DiagnosticCommands, CombatFeedbackSlot CombatFeedback, KeyBindings KeyBindings, FrameProfiler FrameProfiler, FramebufferResizeController FramebufferResize, Action Log) { public RuntimeCommunicationState Communication => Runtime.CommunicationOwner; public CombatState Combat => Runtime.ActionOwner.Combat; public LocalPlayerState LocalPlayer => Runtime.CharacterOwner.LocalPlayer; } internal interface IGameWindowSettingsDevToolsPublication { void PublishDevTools(DevToolsCompositionOwner value); } internal interface ISettingsDevToolsCompositionFactory { bool IsSupported(IInputContext input); IDevToolsInputContext CreateInputContext( IInputContext input, HostQuiescenceGate quiescence); IImGuiBootstrapper CreateBootstrap( GL gl, IView window, IInputContext input); ImGuiPanelHost CreatePanelHost(); IDevToolsFrameBackend CreateBackend( IImGuiBootstrapper bootstrap, ImGuiPanelHost panels); } internal sealed class RetailSettingsDevToolsCompositionFactory : ISettingsDevToolsCompositionFactory { public bool IsSupported(IInputContext input) => input.Keyboards.Count > 0 && input.Mice.Count > 0 && input.Mice[0].ScrollWheels.Count > 0; public IDevToolsInputContext CreateInputContext( IInputContext input, HostQuiescenceGate quiescence) => new QuiescentInputContext(input, quiescence); public IImGuiBootstrapper CreateBootstrap( GL gl, IView window, IInputContext input) => new ImGuiBootstrapper(gl, window, input); public ImGuiPanelHost CreatePanelHost() => new(); public IDevToolsFrameBackend CreateBackend( IImGuiBootstrapper bootstrap, ImGuiPanelHost panels) => new ImGuiDevToolsFrameBackend(bootstrap, panels); } internal enum SettingsDevToolsCompositionPoint { SettingsApplied, InputContextCreated, BootstrapCreated, InputActivated, PanelHostCreated, VitalsRegistered, ChatRegistered, DebugViewModelCreated, DiagnosticActionsBound, CombatFeedbackBound, DebugRegistered, SettingsViewModelBound, SettingsRegistered, BackendCreated, PresenterCreated, FramebufferBound, InitialLayoutApplied, DevToolsPublished, } /// /// Complete optional developer frontend lifetime. Construction rollback and /// normal shutdown share the same named, retryable release operations. /// internal sealed class DevToolsCompositionOwner : IDisposable { private readonly IDevToolsInputContext _input; private readonly IDevToolsFrameBackend _backend; private readonly ChatVM _chat; private readonly DebugVM _debug; private readonly RuntimeSettingsViewModelBinding? _settings; private readonly CombatFeedbackSlot _combatFeedback; private readonly FramebufferDevToolsBinding _framebuffer; private readonly ResourceShutdownTransaction _frontendShutdown; private bool _ownsInput; private bool _ownsBackend; private bool _ownsChat; private bool _ownsDebug; private bool _ownsSettings; private bool _ownsCombatBinding; private bool _ownsFramebuffer; public DevToolsCompositionOwner( IDevToolsInputContext input, IDevToolsFrameBackend backend, DevToolsFramePresenter presenter, DevToolsCommandBusSource commandBus, VitalsVM vitals, ChatVM chat, DebugVM debug, SettingsDevToolsOptionalDependencies lateBindings, RuntimeSettingsViewModelBinding? settings, CombatFeedbackSlot combatFeedback, FramebufferDevToolsBinding framebuffer) { _input = input ?? throw new ArgumentNullException(nameof(input)); _backend = backend ?? throw new ArgumentNullException(nameof(backend)); Presenter = presenter ?? throw new ArgumentNullException(nameof(presenter)); CommandBus = commandBus ?? throw new ArgumentNullException(nameof(commandBus)); Vitals = vitals ?? throw new ArgumentNullException(nameof(vitals)); _chat = chat ?? throw new ArgumentNullException(nameof(chat)); _debug = debug ?? throw new ArgumentNullException(nameof(debug)); LateBindings = lateBindings ?? throw new ArgumentNullException(nameof(lateBindings)); _settings = settings; _combatFeedback = combatFeedback ?? throw new ArgumentNullException(nameof(combatFeedback)); _framebuffer = framebuffer ?? throw new ArgumentNullException(nameof(framebuffer)); _frontendShutdown = new ResourceShutdownTransaction( new ResourceShutdownStage("developer framebuffer binding", [ new("framebuffer resize binding", ReleaseFramebuffer), ]), new ResourceShutdownStage("developer backend", [ new("ImGui backend", ReleaseBackend), ]), new ResourceShutdownStage("developer view models", [ new("settings view model binding", ReleaseSettings), new("combat feedback binding", ReleaseCombatBinding), new("debug combat subscriptions", ReleaseDebug), new("chat transcript subscription", ReleaseChat), ])); } public DevToolsFramePresenter Presenter { get; } public DevToolsCommandBusSource CommandBus { get; } public VitalsVM Vitals { get; } public DebugVM Debug => _debug; public SettingsDevToolsOptionalDependencies LateBindings { get; } public bool IsDisposalComplete => _frontendShutdown.IsComplete && !_ownsInput; internal bool IsFrontendDisposalComplete => _frontendShutdown.IsComplete; public void AdoptInput() => _ownsInput = true; public void AdoptBackend() => _ownsBackend = true; public void AdoptChat() => _ownsChat = true; public void AdoptDebug() => _ownsDebug = true; public void AdoptSettings() => _ownsSettings = _settings is not null; public void AdoptCombatBinding() => _ownsCombatBinding = true; public void AdoptFramebuffer() => _ownsFramebuffer = true; public void CompleteOwnership() { if (!_ownsInput || !_ownsBackend || !_ownsChat || !_ownsDebug || !_ownsCombatBinding || !_ownsFramebuffer || (_settings is not null && !_ownsSettings)) { throw new InvalidOperationException( "Developer composition ownership is incomplete."); } } public void DeactivateInput() { CommandBus.Deactivate(); LateBindings.PlayerModeCommands.Deactivate(); LateBindings.FrameDiagnostics.Deactivate(); LateBindings.WorldEntities.Deactivate(); if (_ownsInput) _input.Deactivate(); } public void DetachInput() { if (!_ownsInput) return; _input.Dispose(); if (!_input.IsDisposalComplete) throw new InvalidOperationException( "Developer input callback cleanup remains incomplete."); _ownsInput = false; } public void DisposeFrontend() => _frontendShutdown.CompleteOrThrow(); public void Dispose() { DeactivateInput(); DisposeFrontend(); DetachInput(); } private void ReleaseFramebuffer() { if (!_ownsFramebuffer) return; _framebuffer.Dispose(); _ownsFramebuffer = false; } private void ReleaseCombatBinding() { if (!_ownsCombatBinding) return; _combatFeedback.Unbind(_debug); _ownsCombatBinding = false; } private void ReleaseSettings() { if (!_ownsSettings) return; _settings!.Dispose(); _ownsSettings = false; } private void ReleaseDebug() { if (!_ownsDebug) return; _debug.Dispose(); _ownsDebug = false; } private void ReleaseChat() { if (!_ownsChat) return; _chat.Dispose(); _ownsChat = false; } private void ReleaseBackend() { if (!_ownsBackend) return; _backend.Dispose(); _ownsBackend = false; } } internal sealed class CombatFeedbackBinding : IDisposable { private readonly CombatFeedbackSlot _owner; private readonly DebugVM _viewModel; private bool _disposed; public CombatFeedbackBinding(CombatFeedbackSlot owner, DebugVM viewModel) { _owner = owner ?? throw new ArgumentNullException(nameof(owner)); _viewModel = viewModel ?? throw new ArgumentNullException(nameof(viewModel)); _owner.Bind(_viewModel); } public void Dispose() { if (_disposed) return; _owner.Unbind(_viewModel); _disposed = true; } } /// /// Production Phase 3. Settings are required; the optional developer frontend /// may be abandoned only after every locally acquired edge has been removed. /// internal sealed class SettingsDevToolsCompositionPhase : ISettingsDevToolsCompositionPhase< GameWindowPlatformResult, HostInputCameraResult, ContentEffectsAudioResult, SettingsDevToolsResult> { private readonly SettingsDevToolsDependencies _dependencies; private readonly IGameWindowSettingsDevToolsPublication _publication; private readonly ISettingsDevToolsCompositionFactory _factory; private readonly Action? _faultInjection; public SettingsDevToolsCompositionPhase( SettingsDevToolsDependencies dependencies, IGameWindowSettingsDevToolsPublication publication, ISettingsDevToolsCompositionFactory? factory = null, Action? faultInjection = null) { _dependencies = dependencies ?? throw new ArgumentNullException(nameof(dependencies)); _publication = publication ?? throw new ArgumentNullException(nameof(publication)); _factory = factory ?? new RetailSettingsDevToolsCompositionFactory(); _faultInjection = faultInjection; } public SettingsDevToolsResult Compose( GameWindowPlatformResult platform, HostInputCameraResult host, ContentEffectsAudioResult content) { ArgumentNullException.ThrowIfNull(platform); ArgumentNullException.ThrowIfNull(host); ArgumentNullException.ThrowIfNull(content); _dependencies.Settings.ApplyStartup(_dependencies.StartupTarget); Fault(SettingsDevToolsCompositionPoint.SettingsApplied); DevToolsCompositionOwner? devTools = _dependencies.DevTools is { } optional ? ComposeOptionalDevTools(platform, host, optional) : null; return new SettingsDevToolsResult( _dependencies.Settings.ResolvedQuality, devTools); } private DevToolsCompositionOwner? ComposeOptionalDevTools( GameWindowPlatformResult platform, HostInputCameraResult host, SettingsDevToolsOptionalDependencies optional) { var scope = new CompositionAcquisitionScope(); bool published = false; try { if (!_factory.IsSupported(platform.Input)) { _dependencies.Log( "devtools: keyboard, mouse, or scroll input unavailable — devtools disabled"); return null; } var inputLease = scope.Acquire( "developer input context", () => _factory.CreateInputContext( platform.Input, _dependencies.HostQuiescence), static value => value.Dispose()); IDevToolsInputContext input = inputLease.Resource; Fault(SettingsDevToolsCompositionPoint.InputContextCreated); var bootstrapLease = scope.Acquire( "ImGui bootstrap", () => _factory.CreateBootstrap( platform.Graphics, _dependencies.Window, input), static value => value.Dispose()); IImGuiBootstrapper bootstrap = bootstrapLease.Resource; Fault(SettingsDevToolsCompositionPoint.BootstrapCreated); input.Activate(); Fault(SettingsDevToolsCompositionPoint.InputActivated); ImGuiPanelHost panelHost = _factory.CreatePanelHost(); Fault(SettingsDevToolsCompositionPoint.PanelHostCreated); var vitals = new VitalsVM( _dependencies.Combat, _dependencies.LocalPlayer); var vitalsPanel = new VitalsPanel(vitals); panelHost.Register(vitalsPanel); Fault(SettingsDevToolsCompositionPoint.VitalsRegistered); var chatLease = scope.Acquire( "developer chat view model", () => new ChatVM( _dependencies.Communication.Chat, commandTargets: _dependencies.Communication.CommandTargets) { FpsProvider = () => optional.Facts.Fps, PositionProvider = () => optional.Facts.PlayerPosition, }, static value => value.Dispose()); ChatVM chatVm = chatLease.Resource; var chatPanel = new ChatPanel(chatVm); panelHost.Register(chatPanel); Fault(SettingsDevToolsCompositionPoint.ChatRegistered); var debugLease = scope.Acquire( "developer debug view model", () => CreateDebugViewModel(optional.Facts), static value => value.Dispose()); DebugVM debugVm = debugLease.Resource; Fault(SettingsDevToolsCompositionPoint.DebugViewModelCreated); debugVm.CycleTimeOfDay = _dependencies.DiagnosticCommands.CycleTimeOfDay; debugVm.CycleWeather = _dependencies.DiagnosticCommands.CycleWeather; debugVm.ToggleCollisionWires = _dependencies.DiagnosticCommands.ToggleCollisionWireframes; debugVm.ToggleFlyMode = optional.PlayerModeCommands.ToggleFlyOrChase; Fault(SettingsDevToolsCompositionPoint.DiagnosticActionsBound); var combatBindingLease = scope.Acquire( "combat feedback binding", () => new CombatFeedbackBinding( _dependencies.CombatFeedback, debugVm), static value => value.Dispose()); Fault(SettingsDevToolsCompositionPoint.CombatFeedbackBound); var debugPanel = new DebugPanel(debugVm); panelHost.Register(debugPanel); Fault(SettingsDevToolsCompositionPoint.DebugRegistered); RuntimeSettingsViewModelBinding? settingsBinding = null; SettingsPanel? settingsPanel = null; CompositionAcquisitionScope.CompositionAcquisitionLease? settingsLease = null; if (host.InputDispatcher is not null && optional.KeyBindingTarget is not null) { settingsLease = scope.Acquire( "settings view model binding", () => _dependencies.Settings.CreateViewModelBinding( _dependencies.KeyBindings, host.InputDispatcher, optional.KeyBindingTarget.Apply), static value => value.Dispose()); settingsBinding = settingsLease.Resource; Fault(SettingsDevToolsCompositionPoint.SettingsViewModelBound); settingsPanel = new SettingsPanel(settingsBinding.ViewModel); panelHost.Register(settingsPanel); Fault(SettingsDevToolsCompositionPoint.SettingsRegistered); } var backendLease = scope.Acquire( "developer frame backend", () => _factory.CreateBackend(bootstrap, panelHost), static value => value.Dispose()); IDevToolsFrameBackend backend = backendLease.Resource; bootstrapLease.Transfer(); Fault(SettingsDevToolsCompositionPoint.BackendCreated); var cameraMenu = new DevToolsCameraMenuOperations( host.CameraController, optional.PlayerModeCommands); var commandBus = new DevToolsCommandBusSource(); var presenter = new DevToolsFramePresenter( backend, cameraMenu, commandBus, _dependencies.FrameProfiler, new DevToolsPanelSet( vitalsPanel, chatPanel, debugPanel, debugVm, settingsPanel)); Fault(SettingsDevToolsCompositionPoint.PresenterCreated); var framebufferLease = scope.Acquire( "developer framebuffer binding", () => new FramebufferDevToolsBinding( _dependencies.FramebufferResize, new DevToolsFramebufferTarget(presenter)), static value => value.Dispose()); Fault(SettingsDevToolsCompositionPoint.FramebufferBound); presenter.ResetLayout( _dependencies.Window.Size.X, _dependencies.Window.Size.Y, DevToolsPanelLayoutCondition.FirstUseEver); Fault(SettingsDevToolsCompositionPoint.InitialLayoutApplied); var ownerLease = scope.Acquire( "developer composition owner", () => new DevToolsCompositionOwner( input, backend, presenter, commandBus, vitals, chatVm, debugVm, optional, settingsBinding, _dependencies.CombatFeedback, framebufferLease.Resource), static value => value.Dispose()); DevToolsCompositionOwner owner = ownerLease.Resource; inputLease.Transfer(); owner.AdoptInput(); backendLease.Transfer(); owner.AdoptBackend(); chatLease.Transfer(); owner.AdoptChat(); debugLease.Transfer(); owner.AdoptDebug(); combatBindingLease.Transfer(); owner.AdoptCombatBinding(); if (settingsLease is not null) { settingsLease.Transfer(); owner.AdoptSettings(); } framebufferLease.Transfer(); owner.AdoptFramebuffer(); owner.CompleteOwnership(); owner = ownerLease.Publish( _publication.PublishDevTools); published = true; Fault(SettingsDevToolsCompositionPoint.DevToolsPublished); scope.Complete(); _dependencies.Log( "devtools: ImGui panel host ready " + "(VitalsPanel + ChatPanel + DebugPanel + SettingsPanel registered)"); return owner; } catch (Exception failure) { if (published) throw; try { scope.RollbackAndThrow(failure); } catch (Exception rolledBack) when (!HasIncompleteCleanup(rolledBack)) { _dependencies.Log( $"devtools: ImGui init failed: {rolledBack.Message} — devtools disabled"); return null; } throw new System.Diagnostics.UnreachableException(); } } private DebugVM CreateDebugViewModel(IDevToolsRuntimeFacts facts) { ArgumentNullException.ThrowIfNull(facts); return new DebugVM( () => facts.PlayerPosition, () => facts.PlayerHeadingDegrees, () => facts.PlayerCellId, () => facts.PlayerOnGround, () => facts.InPlayerMode, () => facts.InFlyMode, () => facts.VerticalVelocity, () => facts.EntityCount, () => facts.AnimatedCount, () => facts.VisibleLandblocks, () => facts.TotalLandblocks, () => facts.ShadowObjectCount, () => facts.NearestObjectDistance, () => facts.NearestObjectLabel, () => facts.Colliding, () => facts.CollisionWireframesVisible, () => facts.StreamingRadius, () => facts.MouseSensitivity, () => facts.ChaseDistance, () => facts.RmbOrbitHeld, () => facts.HourName, () => facts.DayFraction, () => facts.Weather, () => facts.ActiveLights, () => facts.RegisteredLights, () => facts.ParticleCount, () => facts.Fps, () => facts.FrameMilliseconds, _dependencies.Combat); } private static bool HasIncompleteCleanup(Exception failure) { if (failure is ImGuiBootstrapperConstructionException) return true; if (failure is IRetryableResourceCleanup cleanup && !cleanup.IsCleanupComplete) { return true; } if (failure is AggregateException aggregate) return aggregate.InnerExceptions.Any(HasIncompleteCleanup); return failure.InnerException is { } inner && HasIncompleteCleanup(inner); } private void Fault(SettingsDevToolsCompositionPoint point) => _faultInjection?.Invoke(point); }