diff --git a/AcDream.slnx b/AcDream.slnx index b9ed96d4..d28db1d2 100644 --- a/AcDream.slnx +++ b/AcDream.slnx @@ -11,7 +11,6 @@ - diff --git a/src/AcDream.App/AcDream.App.csproj b/src/AcDream.App/AcDream.App.csproj index 2c517d49..5a04d5a4 100644 --- a/src/AcDream.App/AcDream.App.csproj +++ b/src/AcDream.App/AcDream.App.csproj @@ -52,7 +52,6 @@ - diff --git a/src/AcDream.App/Composition/FrameRootComposition.cs b/src/AcDream.App/Composition/FrameRootComposition.cs index fd6cde4d..0e56982c 100644 --- a/src/AcDream.App/Composition/FrameRootComposition.cs +++ b/src/AcDream.App/Composition/FrameRootComposition.cs @@ -433,16 +433,17 @@ internal sealed class FrameRootCompositionPhase // Campaign V slice V6j: no debug-line renderer on the Vulkan arm. // DrawAndPublish flushes it INSIDE the world phase and that // renderer opens its own pass, which the frame's one backbuffer - // pass forbids. The collision-wireframe toggle is DevTools-only - // and DevTools is not composed there, so nothing is lost — - // composing it would throw on the first wireframe frame rather - // than silently misdraw (plan §5.5.14 item 7). + // pass forbids. The collision-wireframe toggle was DevTools-only + // and the ImGui DevTools frontend is gone as of Campaign V slice + // V11, so nothing is lost — composing it would throw on the + // first wireframe frame rather than silently misdraw (plan + // §5.5.14 item 7). gl is not null ? foundation.DebugLines : null, d.PhysicsEngine, d.PlayerMode, d.PlayerController, d.DebugVmRenderFacts, - settings.DevTools is not null); + debugVmConsumerActive: false); var worldScenePasses = new WorldScenePassExecutor( worldPassSurface, worldFrameGlState, @@ -570,11 +571,15 @@ internal sealed class FrameRootCompositionPhase live.PaperdollPresenter, live.CreatureAppraisalPresenter), retainedGameplayUi, - settings.DevTools?.Presenter, + // The ImGui developer-tools frontend was removed at Campaign V + // slice V11; this optional hook is unbound until a follow-up + // re-homes it onto the retained UI (IDevToolsFrameLifecycle stays + // as the seam). + devTools: null, privateScreenshot); var framePreparation = new RenderFramePreparationController( renderFrameResources, - settings.DevTools?.Presenter, + devTools: null, renderWeatherFrame); IRenderFramePostDiagnosticsPhase postDiagnostics = renderSceneShadowComparison is not null @@ -602,8 +607,7 @@ internal sealed class FrameRootCompositionPhase privatePresentation, live.FrameDiagnostics, postDiagnostics, - (IRenderFrameFailureRecovery?)settings.DevTools?.Presenter - ?? NullRenderFrameFailureRecovery.Instance); + NullRenderFrameFailureRecovery.Instance); Fault(FrameRootCompositionPoint.RenderRootCreated); var liveFrameCoordinator = new RetailLiveFrameCoordinator( diff --git a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs index f19f700b..792ad2e0 100644 --- a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs +++ b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs @@ -426,7 +426,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory d.Character.LocalPlayer, playerGuid: () => d.PlayerIdentity.ServerGuid, activeToonName: () => d.Settings.ActiveToonKey, - fallbackSheet: Studio.SampleData.SampleCharacter, + fallbackSheet: SampleData.SampleCharacter, canSendRaise: () => late.GameRuntime.IsInWorld, sendRaiseAttribute: (statId, cost) => late.GameRuntime.Advance( diff --git a/src/AcDream.App/Composition/SessionPlayerComposition.cs b/src/AcDream.App/Composition/SessionPlayerComposition.cs index ca1ac387..cd6ae920 100644 --- a/src/AcDream.App/Composition/SessionPlayerComposition.cs +++ b/src/AcDream.App/Composition/SessionPlayerComposition.cs @@ -740,15 +740,6 @@ internal sealed class SessionPlayerCompositionPhase d.MovementDiagnostics, d.Character.MovementSkills, d.ViewportAspect); - if (d.SettingsDevTools.DevTools is { } devTools) - { - bindings.Adopt( - "developer player mode", - devTools.LateBindings.PlayerModeCommands.BindOwned(playerMode)); - bindings.Adopt( - "developer command bus", - devTools.CommandBus.BindOwned(liveSessionSource)); - } var playerModeAutoEntry = new PlayerModeAutoEntry( new LivePlayerModeAutoEntryContext( liveSessionSource, @@ -832,8 +823,7 @@ internal sealed class SessionPlayerCompositionPhase static value => value.Dispose()); AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? vitals = - d.SettingsDevTools.DevTools?.Vitals - ?? interaction.RetainedUi?.Vitals; + interaction.RetainedUi?.Vitals; var sessionRuntimeFactory = new LiveSessionRuntimeFactory( new LiveSessionPlayerRuntime( d.PlayerIdentity, @@ -893,9 +883,10 @@ internal sealed class SessionPlayerCompositionPhase d.Options.LivePass ?? string.Empty)); Fault(SessionPlayerCompositionPoint.SessionHostCreated); - Action? debugToast = d.SettingsDevTools.DevTools is { } devOwner - ? message => devOwner.Debug.AddToast(message) - : null; + // The ImGui developer-tools debug toast sink was removed at Campaign V + // slice V11 along with the rest of the ImGui frontend; there is no + // replacement toast surface yet. + Action? debugToast = null; var combatModeOperations = new LiveCombatModeOperations( new LiveSessionCombatModeAuthority(sessionHost), new LocalPlayerCombatEquipmentSource( @@ -954,8 +945,7 @@ internal sealed class SessionPlayerCompositionPhase var commands = new GameplayInputCommandController( new RetainedGameplayWindowCommands( interaction.RetainedUi?.Runtime), - new DevToolsGameplayCommands( - d.SettingsDevTools.DevTools?.Presenter), + new DevToolsGameplayCommands(), runtimeDiagnostics, new PlayerModeGameplayCommands( d.PlayerMode, diff --git a/src/AcDream.App/Composition/SettingsDevToolsComposition.cs b/src/AcDream.App/Composition/SettingsDevToolsComposition.cs index 98c134f8..32980581 100644 --- a/src/AcDream.App/Composition/SettingsDevToolsComposition.cs +++ b/src/AcDream.App/Composition/SettingsDevToolsComposition.cs @@ -1,336 +1,27 @@ -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; +/// +/// Resolved display/audio/quality settings for the session. The optional +/// ImGui developer-tools frontend that used to compose here (VitalsPanel, +/// ChatPanel, DebugPanel, SettingsPanel via AcDream.UI.ImGui) was +/// removed at Campaign V slice V11 along with the OpenGL backend it +/// required — see docs/plans/2026-07-27-vulkan-campaign.md. A follow-up +/// re-homes the Settings and Debug panels onto the retained UI through a new +/// IPanelRenderer implementation; until then keybind remapping falls +/// back to editing keybinds.json. +/// 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); + AcDream.UI.Abstractions.Settings.QualitySettings ResolvedQuality); 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, -} + IRuntimeSettingsStartupTarget StartupTarget); /// -/// 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. +/// Production Phase 3: applies the resolved startup display/audio settings. /// internal sealed class SettingsDevToolsCompositionPhase : ISettingsDevToolsCompositionPhase< @@ -340,20 +31,10 @@ internal sealed class SettingsDevToolsCompositionPhase : 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) + public SettingsDevToolsCompositionPhase(SettingsDevToolsDependencies dependencies) { _dependencies = dependencies ?? throw new ArgumentNullException(nameof(dependencies)); - _publication = publication ?? throw new ArgumentNullException(nameof(publication)); - _factory = factory ?? new RetailSettingsDevToolsCompositionFactory(); - _faultInjection = faultInjection; } public SettingsDevToolsResult Compose( @@ -366,282 +47,6 @@ internal sealed class SettingsDevToolsCompositionPhase : 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); + return new SettingsDevToolsResult(_dependencies.Settings.ResolvedQuality); } - - 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", - // ImGui is a GL-only frontend and is not ported to Vulkan — the - // campaign deletes it at slice V11 — so a Vulkan host composes - // no DevTools at all and never reaches here. - () => _factory.CreateBootstrap( - platform.Graphics.RequireGl("developer UI"), - _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); } diff --git a/src/AcDream.App/Input/GameplayInputCommandController.cs b/src/AcDream.App/Input/GameplayInputCommandController.cs index ce07257a..ca4fa4fc 100644 --- a/src/AcDream.App/Input/GameplayInputCommandController.cs +++ b/src/AcDream.App/Input/GameplayInputCommandController.cs @@ -30,16 +30,26 @@ internal interface IDevToolsGameplayCommands void ToggleSettingsPanel(); } -internal sealed class DevToolsGameplayCommands(DevToolsFramePresenter? presenter) - : IDevToolsGameplayCommands +/// +/// The ImGui developer-tools frontend these commands used to forward to was +/// removed at Campaign V slice V11. A follow-up re-homes the Settings and +/// Debug panels onto the retained UI through a new IPanelRenderer +/// implementation; until then these are no-ops so the frozen gameplay-action +/// priority graph keeps a single, always-valid handoff target. +/// +internal sealed class DevToolsGameplayCommands : IDevToolsGameplayCommands { - private readonly DevToolsFramePresenter? _presenter = presenter; + public void ToggleDebugPanel() + { + } - public void ToggleDebugPanel() => _presenter?.ToggleDebugPanel(); + public void FocusChatInput() + { + } - public void FocusChatInput() => _presenter?.FocusChatInput(); - - public void ToggleSettingsPanel() => _presenter?.ToggleSettingsPanel(); + public void ToggleSettingsPanel() + { + } } internal interface IPlayerModeGameplayCommands diff --git a/src/AcDream.App/Input/InputCaptureSources.cs b/src/AcDream.App/Input/InputCaptureSources.cs index ff9e34a5..f4d9b4de 100644 --- a/src/AcDream.App/Input/InputCaptureSources.cs +++ b/src/AcDream.App/Input/InputCaptureSources.cs @@ -9,17 +9,21 @@ internal interface IInputCaptureSource bool DevToolsWantCaptureKeyboard { get; } } +/// +/// The ImGui developer-tools frontend this used to poll for focus capture was +/// removed at Campaign V slice V11; there is no developer UI to steal focus +/// from world/retained-UI input anymore, so this always reports false. +/// internal sealed class DevToolsInputCaptureSource { - private readonly bool _enabled; + public DevToolsInputCaptureSource(bool enabled) + { + _ = enabled; + } - public DevToolsInputCaptureSource(bool enabled) => _enabled = enabled; + public bool WantCaptureMouse => false; - public bool WantCaptureMouse => - _enabled && ImGuiNET.ImGui.GetIO().WantCaptureMouse; - - public bool WantCaptureKeyboard => - _enabled && ImGuiNET.ImGui.GetIO().WantCaptureKeyboard; + public bool WantCaptureKeyboard => false; } internal sealed class RetainedUiInputCaptureSlot diff --git a/src/AcDream.App/Platform/GraphicalHostPlatformServices.cs b/src/AcDream.App/Platform/GraphicalHostPlatformServices.cs index f498debf..ca466c6d 100644 --- a/src/AcDream.App/Platform/GraphicalHostPlatformServices.cs +++ b/src/AcDream.App/Platform/GraphicalHostPlatformServices.cs @@ -94,13 +94,11 @@ internal sealed record GraphicalHostPlatformServices( GraphicalHostOperatingSystem.Windows => [ new("window/input", "glfw3.dll"), - new("developer UI", "cimgui.dll"), new("audio", "soft_oal.dll"), ], GraphicalHostOperatingSystem.Linux => [ new("window/input", "libglfw.so.3"), - new("developer UI", "libcimgui.so"), new("audio", "libopenal.so"), ], _ => throw new ArgumentOutOfRangeException( diff --git a/src/AcDream.App/Program.cs b/src/AcDream.App/Program.cs index 2c77e4d4..7380feb1 100644 --- a/src/AcDream.App/Program.cs +++ b/src/AcDream.App/Program.cs @@ -13,24 +13,6 @@ ApplicationPathSet applicationPaths = graphicalPlatform.Paths; IReadOnlyList migratedConfigurationFiles = GraphicalLegacyConfigurationMigrator.Migrate(applicationPaths); -if (args.Length >= 1 && args[0] == "ui-studio") -{ - var so = AcDream.App.Studio.StudioOptions.Parse(args[1..]); - using var sw = new AcDream.App.Studio.StudioWindow( - so, - applicationPaths); - try - { - sw.Run(); - return 0; - } - catch (NotSupportedException error) - { - Console.Error.WriteLine(error.Message); - return 4; - } -} - Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .WriteTo.Console() @@ -62,6 +44,14 @@ if (string.IsNullOrWhiteSpace(datDir)) // raw env vars. See docs/architecture/code-structure.md §2 Rule 4. var runtimeOptions = RuntimeOptions.FromEnvironment(datDir); +if (runtimeOptions.DevTools) +{ + Log.Information( + "ACDREAM_DEVTOOLS=1: the ImGui developer UI was removed at Campaign V " + + "slice V11 along with the OpenGL backend it required; this flag now " + + "only selects the optional Vulkan validation/debug-utils extensions."); +} + var worldGameState = new AcDream.Core.Plugins.WorldGameState(); var worldEvents = new AcDream.Core.Plugins.WorldEvents(); var uiRegistry = new AcDream.App.Plugins.BufferedUiRegistry(); diff --git a/src/AcDream.App/Rendering/DevToolsFramePresenter.cs b/src/AcDream.App/Rendering/DevToolsFramePresenter.cs deleted file mode 100644 index c6f2de91..00000000 --- a/src/AcDream.App/Rendering/DevToolsFramePresenter.cs +++ /dev/null @@ -1,491 +0,0 @@ -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); - } -} diff --git a/src/AcDream.App/Rendering/FramebufferResizeController.cs b/src/AcDream.App/Rendering/FramebufferResizeController.cs index 7e89da42..fe71880c 100644 --- a/src/AcDream.App/Rendering/FramebufferResizeController.cs +++ b/src/AcDream.App/Rendering/FramebufferResizeController.cs @@ -37,19 +37,6 @@ internal interface IFramebufferDevToolsTarget void ResetLayout(int width, int height); } -internal sealed class DevToolsFramebufferTarget(DevToolsFramePresenter presenter) - : IFramebufferDevToolsTarget -{ - private readonly DevToolsFramePresenter _presenter = presenter - ?? throw new ArgumentNullException(nameof(presenter)); - - public void ResetLayout(int width, int height) => - _presenter.ResetLayout( - width, - height, - DevToolsPanelLayoutCondition.Always); -} - /// Expected-owner lease for the optional Phase-3 resize edge. internal sealed class FramebufferDevToolsBinding : IDisposable { @@ -124,8 +111,9 @@ internal sealed class FramebufferResizeController return; // Frozen order: GL viewport, shared aspect publication, camera, then - // optional ImGui layout reset. Late binding never replays an earlier - // resize transition. + // the optional developer-tools layout reset (unbound since Campaign V + // slice V11 removed the ImGui frontend). Late binding never replays an + // earlier resize transition. _viewport?.ResizeViewport(width, height); _viewportAspect.Update(width, height); _camera?.SetAspect(width / (float)height); diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 45aa9451..e5f4e510 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -26,7 +26,6 @@ public sealed class GameWindow : IGameWindowPlatformPublication, IGameWindowHostInputCameraPublication, IGameWindowContentEffectsAudioPublication, - IGameWindowSettingsDevToolsPublication, IGameWindowWorldRenderPublication, IGameWindowInteractionRetainedUiPublication, IGameWindowLivePresentationPublication, @@ -91,11 +90,11 @@ public sealed class GameWindow : private readonly AcDream.App.Rendering.WorldSceneDebugState _worldSceneDebugState = new(); - // Phase I.2: the old StbTrueTypeSharp DebugOverlay was deleted in - // favor of the ImGui-backed DebugPanel (see _debugVm below). The - // TextRenderer + BitmapFont fields stay alive because they're shared + // Phase I.2: the old StbTrueTypeSharp DebugOverlay was deleted in favor of + // the ImGui-backed DebugPanel, which Campaign V slice V11 removed in turn. + // The TextRenderer + BitmapFont fields stay alive because they're shared // with UiHost and reserved for the future world-space HUD (D.6 — - // damage floaters, name plates) where ImGui can't reach into the 3D + // damage floaters, name plates) where ImGui couldn't reach into the 3D // scene. They are no longer used for any debug overlay. private TextRenderer? _textRenderer; private BitmapFont? _debugFont; @@ -379,12 +378,9 @@ public sealed class GameWindow : public AcDream.Core.Player.LocalPlayerState LocalPlayer => _runtimeCharacter.LocalPlayer; - // Phase D.2a — ImGui devtools UI overlay. Null unless ACDREAM_DEVTOOLS=1. - // See docs/plans/2026-04-24-ui-framework.md for the staged UI strategy. - private AcDream.App.Rendering.DevToolsFramePresenter? _devToolsFramePresenter; - private AcDream.App.Rendering.DevToolsCommandBusSource? _devToolsCommandBus; - private DevToolsCompositionOwner? _devToolsComposition; - private AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? _vitalsVm; + // Phase D.2a — ImGui devtools UI overlay. Removed at Campaign V slice V11; + // see docs/plans/2026-04-24-ui-framework.md for the staged UI strategy and + // docs/plans/2026-07-27-vulkan-campaign.md for the removal. private AcDream.UI.Abstractions.Panels.Chat.ChatVM? _retailChatVm; // Phase D.2b — retained host + composition runtime. Null unless ACDREAM_RETAIL_UI=1. private AcDream.App.UI.UiHost? _uiHost; @@ -418,19 +414,13 @@ public sealed class GameWindow : _creatureAppraisalFramePresenter; // Phase D.2b Task 9 — plugin UI registrations buffered before OnLoad; drained in OnLoad. private readonly AcDream.App.Plugins.BufferedUiRegistry? _uiRegistry; - // Phase I.2: ImGui debug panel ViewModel. The devtools presenter owns - // its panel; the VM remains here because runtime feedback producers bind - // directly to it during composition. - private AcDream.UI.Abstractions.Panels.Debug.DebugVM? _debugVm; - // DevToolsEnabled reads through typed RuntimeOptions. - // - // Campaign V slice V6h: the developer frontend is ImGui, which is not ported - // to Vulkan and which slice V11 deletes outright, so a Vulkan host composes - // none regardless of ACDREAM_DEVTOOLS. The flag still reaches - // VulkanInstanceFactory, where it selects the optional debug-utils - // instance extensions. - private bool DevToolsEnabled => - _options.DevTools && _options.RenderBackend != RenderBackendKind.Vulkan; + // Campaign V slice V11 deleted the ImGui developer-tools frontend along + // with the OpenGL backend it required, so no host ever composes a + // developer UI regardless of ACDREAM_DEVTOOLS. The flag still reaches + // VulkanGraphicsContext, where it selects the optional debug-utils + // instance/device extensions — see the ACDREAM_DEVTOOLS log line in + // Run() below, which is the one remaining observable effect of the flag. + private const bool DevToolsEnabled = false; // Phase G.1-G.2 world lighting/time state. The environment owner keeps // the clock, selected day group, and weather transitions coherent. @@ -922,26 +912,6 @@ public sealed class GameWindow : _audioSink = value.HookSink; } - void IGameWindowSettingsDevToolsPublication.PublishDevTools( - DevToolsCompositionOwner value) - { - ArgumentNullException.ThrowIfNull(value); - if (_devToolsComposition is not null - || _devToolsFramePresenter is not null - || _devToolsCommandBus is not null - || _debugVm is not null) - { - throw new InvalidOperationException( - "The GameWindow composition shell already owns developer tools."); - } - - _devToolsComposition = value; - _devToolsFramePresenter = value.Presenter; - _devToolsCommandBus = value.CommandBus; - _vitalsVm = value.Vitals; - _debugVm = value.Debug; - } - void IGameWindowWorldRenderPublication.PublishBindlessSupport( BindlessSupport value) => PublishCompositionOwner( @@ -1048,7 +1018,6 @@ public sealed class GameWindow : { _uiHost = retained.Host; _retailUiRuntime = retained.Runtime; - _vitalsVm ??= retained.Vitals; _retailChatVm = retained.Chat; _characterSheetProvider = retained.CharacterSheet; _frameScreenshots = retained.Screenshots; @@ -1347,65 +1316,15 @@ public sealed class GameWindow : Console.Error.WriteLine), this).Compose(platformResult, hostInputCamera), (platformResult, hostInputCamera, contentEffectsAudio) => - { - SettingsDevToolsOptionalDependencies? optionalDevTools = null; - if (DevToolsEnabled) - { - var devToolsWorldEntities = - new DeferredCanonicalWorldEntityCountSource(); - var devToolsFrameDiagnostics = - new DeferredRenderFrameDiagnosticsSource(); - var devToolsPlayerModeCommands = - new DeferredDevToolsPlayerModeCommands(); - var devToolsFacts = new DevToolsRuntimeFacts( - _localPlayerMode, - _playerControllerSlot, - hostInputCamera.CameraController, - devToolsWorldEntities, - _animatedEntities, - _debugVmRenderFacts, - _physicsEngine, - _worldSceneDebugState, - _renderRange, - hostInputCamera.CameraPointerInput, - _worldEnvironment, - Lighting, - contentEffectsAudio.ParticleSystem, - devToolsFrameDiagnostics); - IRuntimeKeyBindingTarget? keyBindingTarget = - hostInputCamera.InputDispatcher is { } settingsDispatcher - ? new RuntimeKeyBindingTarget( - settingsDispatcher, - _applicationPaths.KeyBindingsFile) - : null; - optionalDevTools = new SettingsDevToolsOptionalDependencies( - devToolsFacts, - keyBindingTarget, - devToolsWorldEntities, - devToolsFrameDiagnostics, - devToolsPlayerModeCommands); - } - - return new SettingsDevToolsCompositionPhase( + new SettingsDevToolsCompositionPhase( new SettingsDevToolsDependencies( - _window!, _runtimeSettings, new RuntimeSettingsStartupTargets( new SilkRuntimeDisplayWindowTarget(_window!), _displayFramePacing, hostInputCamera.CameraController, - contentEffectsAudio.Audio?.Engine), - _hostQuiescence, - _runtime, - optionalDevTools, - _runtimeDiagnosticCommands, - _combatFeedback, - _keyBindings, - _frameProfiler, - _framebufferResize, - Console.WriteLine), - this).Compose(platformResult, hostInputCamera, contentEffectsAudio); - }, + contentEffectsAudio.Audio?.Engine))) + .Compose(platformResult, hostInputCamera, contentEffectsAudio), (platformResult, contentEffectsAudio, settingsDevTools) => { const uint initialCenterLandblockId = 0xA9B4FFFFu; @@ -1433,9 +1352,10 @@ public sealed class GameWindow : }, (platformResult, hostInputCamera, contentEffectsAudio, settingsDevTools, worldRender) => { - Action? compositionToast = settingsDevTools.DevTools is { } devTools - ? text => devTools.Debug.AddToast(text) - : null; + // The ImGui developer-tools debug toast sink was removed at + // Campaign V slice V11 along with the rest of the ImGui + // frontend; there is no replacement toast surface yet. + Action? compositionToast = null; return new InteractionRetainedUiCompositionPhase( new InteractionRetainedUiDependencies( _options, @@ -1469,7 +1389,7 @@ public sealed class GameWindow : _window!, viewPlane), _uiFrameDiagnostics, - settingsDevTools.DevTools?.Vitals, + ExistingVitals: null, compositionToast, ClientTimerNow, Console.WriteLine, @@ -1490,9 +1410,10 @@ public sealed class GameWindow : worldRender, interactionUi) => { - Action? compositionToast = settingsDevTools.DevTools is { } devTools - ? text => devTools.Debug.AddToast(text) - : null; + // The ImGui developer-tools debug toast sink was removed at + // Campaign V slice V11 along with the rest of the ImGui + // frontend; there is no replacement toast surface yet. + Action? compositionToast = null; return new LivePresentationCompositionPhase( new LivePresentationDependencies( _options, @@ -1527,8 +1448,8 @@ public sealed class GameWindow : _hookRouter, _renderDiagnosticLog, WorldTime, - settingsDevTools.DevTools?.LateBindings.WorldEntities, - settingsDevTools.DevTools?.LateBindings.FrameDiagnostics, + DevWorldEntities: null, + DevFrameDiagnostics: null, _uiFrameDiagnostics, Console.WriteLine, compositionToast), @@ -1761,7 +1682,6 @@ public sealed class GameWindow : _kbSource, _retailUiLease, _uiHost, - _devToolsComposition, _runtime, _runtimeSettings, _movementInput, @@ -1797,7 +1717,6 @@ public sealed class GameWindow : new RenderShutdownRoots( _gpuFrameFlights, _gpuDevice, - _devToolsComposition, _localPlayerTeleport, _portalTunnelFallback, _paperdollViewportRenderer, diff --git a/src/AcDream.App/Rendering/GameWindowLifetime.cs b/src/AcDream.App/Rendering/GameWindowLifetime.cs index 60368775..8e8f29f1 100644 --- a/src/AcDream.App/Rendering/GameWindowLifetime.cs +++ b/src/AcDream.App/Rendering/GameWindowLifetime.cs @@ -63,7 +63,6 @@ internal sealed record IngressShutdownRoots( RetailUiRuntimeLease RetailUi, // Keeps failed physical UI bindings alive through native-window release. UiHost? RetainedUiHost, - DevToolsCompositionOwner? DevTools, GameRuntime Runtime, RuntimeSettingsController Settings, DispatcherMovementInputSource MovementInput, @@ -102,7 +101,6 @@ internal sealed record LiveShutdownRoots( internal sealed record RenderShutdownRoots( GpuFrameFlightController? FrameFlights, IGpuDevice? GpuDevice, - DevToolsCompositionOwner? DevTools, LocalPlayerTeleportController? LocalTeleport, TransferableResourceSlot PortalTunnelFallback, PaperdollViewportRenderer? Paperdoll, @@ -350,7 +348,6 @@ internal static class GameWindowShutdownManifest Hard("mouse source", () => ingress.MouseSource?.Deactivate()), Hard("keyboard source", () => ingress.KeyboardSource?.Deactivate()), Hard("retained UI input", ingress.RetailUi.QuiesceInput), - Hard("developer tools input", () => ingress.DevTools?.DeactivateInput()), Hard("game runtime session", ingress.Runtime.StopSession), ]), new ResourceShutdownStage("physical ingress cleanup", @@ -359,7 +356,6 @@ internal static class GameWindowShutdownManifest Soft("retained gameplay", () => DisposeRetainedGameplay(ingress.RetainedGameplay)), Soft("gameplay actions", () => DisposeGameplayActions(ingress.GameplayActions)), Soft("retained UI input", ingress.RetailUi.DeactivateInput), - Soft("developer tools input", () => ingress.DevTools?.DetachInput()), Soft("camera pointer", () => DisposeCameraPointer(ingress.CameraPointer)), Soft("dispatcher", () => DisposeDispatcher(ingress)), Soft("mouse source", () => DisposeMouseSource(ingress.MouseSource)), @@ -420,7 +416,6 @@ internal static class GameWindowShutdownManifest ]), new ResourceShutdownStage("render frontends", [ - Hard("developer tools", () => DisposeDevToolsFrontend(render.DevTools)), Hard("portal tunnel", () => { render.LocalTeleport?.Dispose(); @@ -600,12 +595,4 @@ internal static class GameWindowShutdownManifest throw new InvalidOperationException("OpenAL native-resource cleanup remains pending."); } - private static void DisposeDevToolsFrontend(DevToolsCompositionOwner? owner) - { - if (owner is null) - return; - owner.DisposeFrontend(); - if (!owner.IsFrontendDisposalComplete) - throw new InvalidOperationException("Developer-tools frontend cleanup remains incomplete."); - } } diff --git a/src/AcDream.App/Rendering/RenderFramePreparationController.cs b/src/AcDream.App/Rendering/RenderFramePreparationController.cs index bb49b62f..82306d67 100644 --- a/src/AcDream.App/Rendering/RenderFramePreparationController.cs +++ b/src/AcDream.App/Rendering/RenderFramePreparationController.cs @@ -5,6 +5,20 @@ internal interface IRenderWeatherFramePhase void Tick(double deltaSeconds); } +/// +/// Optional per-frame developer-presentation hook. Its one production +/// implementation (the ImGui developer-tools frontend) was removed at +/// Campaign V slice V11; the interface survives as the seam a follow-up +/// re-homing Settings/Debug onto the retained UI will implement, and every +/// current caller already treats it as optional (null-conditional). +/// +internal interface IDevToolsFrameLifecycle : IRenderFrameFailureRecovery +{ + void BeginFrame(float deltaSeconds); + + void Render(double deltaSeconds, int viewportWidth, int viewportHeight); +} + /// /// Preserves the accepted pre-world order after the GPU/resource transaction: /// begin optional developer UI, advance render-time weather, then hand the diff --git a/src/AcDream.App/Studio/DumpLayout.cs b/src/AcDream.App/Studio/DumpLayout.cs deleted file mode 100644 index 1f90f724..00000000 --- a/src/AcDream.App/Studio/DumpLayout.cs +++ /dev/null @@ -1,235 +0,0 @@ -using System.Numerics; -using AcDream.App.UI; - -namespace AcDream.App.Studio; - -// ───────────────────────────────────────────────────────────────────────────── -// DumpLayout — load a panel from the retail UI layout dump -// -// The dump stores every node's rect in ABSOLUTE screen coordinates (the -// panel's design position in the retail UI, not relative to its parent). -// Evidence: for the "inventory" panel, the root node is at x=500,y=138 and -// its direct children are also at x=500,y=161 — the child y=161 is only -// 23 pixels below the parent y=138, which makes sense as a child offset -// (the header row), not as the raw rect. If the rects were parent-relative, -// (500,161) would place the child way off the window. -// -// DumpLayout converts absolute → parent-relative by computing: -// child.Left = child.Rect.X - parent.Rect.X -// child.Top = child.Rect.Y - parent.Rect.Y -// -// The root node (ParentTraversalIndex == null) is placed at (0,0) so the -// whole tree sits at the UiHost origin rather than at the panel's retail -// screen position. -// ───────────────────────────────────────────────────────────────────────────── - -/// -/// Builds a static tree from the retail UI layout dump -/// JSON. The tree is a hierarchy of (draws its -/// sprite) or plain containers (Group nodes), with each -/// node's set to the dump's element_id and -/// set to the widget_kind string. -/// -/// This source is STATIC — no controllers, no FixtureProvider, no live -/// game data. It is a build reference for the UI Studio showing any of the 26 -/// retail windows without needing the production panel wired up. -/// -public static class DumpLayout -{ - /// - /// Parse the dump at , find the panel whose slug - /// matches , and build a tree. - /// - /// - /// maps a RenderSurface id (0x06xxxxxx) to a - /// (GL texture handle, native width, native height) triple — pass - /// RenderStack.ResolveChrome from the studio, or a stub returning - /// (1,1,1) for tests. - /// - /// - /// Returns null and sets on failure. - /// - public static UiElement? Load( - string dumpPath, - string slug, - Func resolve, - out string? error) - { - // ── 1. Parse the dump JSON ──────────────────────────────────────── - var dump = UiDumpModel.Parse(dumpPath); - if (dump is null) - { - error = $"[dump] Failed to parse '{dumpPath}'."; - return null; - } - - // ── 2. Find the requested panel ─────────────────────────────────── - var panel = dump.Panels.FirstOrDefault( - p => string.Equals(p.Slug, slug, StringComparison.OrdinalIgnoreCase)); - if (panel is null) - { - error = $"[dump] Panel slug '{slug}' not found. " + - $"Available: {string.Join(", ", dump.Panels.Select(p => p.Slug))}"; - return null; - } - - if (panel.Nodes.Count == 0) - { - error = $"[dump] Panel '{slug}' has no nodes."; - return null; - } - - // ── 3. Build a traversal-index → node lookup ────────────────────── - var byIndex = new Dictionary(panel.Nodes.Count); - foreach (var n in panel.Nodes) - byIndex[n.TraversalIndex] = n; - - // ── 4. Create UiElement objects for every node ──────────────────── - var elements = new Dictionary(panel.Nodes.Count); - foreach (var node in panel.Nodes) - { - var el = BuildElement(node, resolve); - elements[node.TraversalIndex] = el; - } - - // ── 5. Wire parent–child relationships + set parent-relative coords ─ - UiElement? root = null; - foreach (var node in panel.Nodes) - { - var el = elements[node.TraversalIndex]; - - if (node.ParentTraversalIndex is null) - { - // Root node — place at (0,0) so the tree sits at the UiHost origin. - // The panel's absolute rect offset is discarded here (it was the - // retail design position inside the retail screen, which we don't need). - el.Left = 0f; - el.Top = 0f; - root = el; - } - else - { - // Non-root: convert absolute → parent-relative by subtracting parent rect. - // child.Left = child.Rect.X - parent.Rect.X - // child.Top = child.Rect.Y - parent.Rect.Y - // This preserves the visual layout inside each group without placing the - // entire panel at its retail screen origin. - var parentNode = byIndex[node.ParentTraversalIndex.Value]; - el.Left = node.Rect.X - parentNode.Rect.X; - el.Top = node.Rect.Y - parentNode.Rect.Y; - - var parentEl = elements[node.ParentTraversalIndex.Value]; - parentEl.AddChild(el); - } - } - - if (root is null) - { - error = $"[dump] Panel '{slug}': no root node found (all nodes have a parent)."; - return null; - } - - // Give the root the full panel dimensions (from the dump's width/height record). - root.Width = panel.Width; - root.Height = panel.Height; - - error = null; - return root; - } - - // ── Private helpers ─────────────────────────────────────────────────────── - - private static UiElement BuildElement( - DumpNode node, - Func resolve) - { - uint imageId = UiDumpModel.PickImageId(node); - var kind = node.WidgetKind ?? "Group"; - - UiElement el; - if (imageId != 0 && !string.Equals(kind, "Group", StringComparison.OrdinalIgnoreCase)) - { - // Sprite/Button/Scrollbar/Slider — create a sprite-drawing element. - el = new DumpSpriteElement(imageId, resolve) - { - Name = kind, - ClickThrough = true, // static mockup; no behavior - Anchors = AnchorEdges.None, - }; - } - else - { - // Group (or sprite without an image) — plain container, no own draw. - el = new DumpGroupElement() - { - Name = kind, - ClickThrough = true, - Anchors = AnchorEdges.None, - }; - } - - // EventId is set from the dump's element_id (cast to uint — the decimal - // values in the JSON represent the same dat handle used at runtime). - el.EventId = (uint)node.ElementId; - el.Left = node.Rect.X; // overwritten by caller per root/child logic - el.Top = node.Rect.Y; - el.Width = node.Rect.Width; - el.Height = node.Rect.Height; - - return el; - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// DumpSpriteElement — minimal element that draws a single sprite -// ───────────────────────────────────────────────────────────────────────────── - -/// -/// Draws a single sprite at its native size tiled to fill -/// × . Used for Sprite/Button/Scrollbar/Slider nodes from -/// the retail UI dump. -/// -/// We do NOT reuse here because -/// that class requires an ElementInfo with a populated StateMedia -/// dictionary — the dat-import plumbing — which is not needed for a static dump -/// preview. A minimal subclass keeps the code simpler and the dependency surface -/// smaller. -/// -internal sealed class DumpSpriteElement : UiElement -{ - private readonly uint _imageId; - private readonly Func _resolve; - - public DumpSpriteElement(uint imageId, Func resolve) - { - _imageId = imageId; - _resolve = resolve; - } - - protected override void OnDraw(UiRenderContext ctx) - { - if (_imageId == 0) return; - - var (tex, tw, th) = _resolve(_imageId); - if (tex == 0 || tw == 0 || th == 0) return; - - // Tile at native resolution (same as UiDatElement.OnDraw — UV-repeat on both - // axes via GL_REPEAT, Width/tw and Height/th tile the texture). - ctx.DrawSprite(tex, 0, 0, Width, Height, - 0, 0, Width / tw, Height / th, Vector4.One); - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// DumpGroupElement — pure container (Group nodes from the dump) -// ───────────────────────────────────────────────────────────────────────────── - -/// -/// Container element for dump Group nodes — no own draw, just hosts children. -/// Extending UiElement directly (no OnDraw override) gives transparent groups, -/// which matches Group nodes in the retail layout that have no background sprite. -/// -internal sealed class DumpGroupElement : UiElement -{ - // No OnDraw — completely transparent container. -} diff --git a/src/AcDream.App/Studio/FixtureProvider.cs b/src/AcDream.App/Studio/FixtureProvider.cs deleted file mode 100644 index eb375b40..00000000 --- a/src/AcDream.App/Studio/FixtureProvider.cs +++ /dev/null @@ -1,225 +0,0 @@ -using AcDream.App.Rendering; -using AcDream.App.UI; -using AcDream.App.UI.Layout; -using AcDream.Core.Combat; -using AcDream.Core.Items; -using AcDream.Runtime.Gameplay; -using AcDream.UI.Abstractions.Panels.Settings; -using DatReaderWriter; -using AcDream.Content; - -namespace AcDream.App.Studio; - -/// -/// Populates a loaded panel with sample data by calling the production -/// controller Bind methods against . -/// -/// -/// The studio is intentionally thin — there is no live game session, no -/// server connection, and no network. FixtureProvider bridges that gap by -/// feeding static fixtures (vitals percentages, a fake inventory, empty -/// shortcut lists) so the bound widgets show plausible state instead of -/// empty zeroes. -/// -/// -/// -/// IconIds approach: raw-resolve stub — resolve the base iconId -/// via and return the GL handle -/// directly. This is intentionally simpler than GameWindow's full -/// (5-layer composite). The raw icon is enough -/// to confirm the grid cells draw something in the studio; the full -/// compositor is the live-game concern, not the layout preview concern. -/// -/// -public static class FixtureProvider -{ - /// - /// Populate with sample data appropriate for - /// . Calls the production controller Bind - /// methods so the panel's widgets drive off the same code path as the - /// full game. - /// - /// The LayoutDesc dat id used to import the layout. - /// The imported layout whose widgets to populate. - /// The live render stack (for - /// and ). - /// A already seeded by - /// . - /// The live DAT collection used to resolve per-list empty-slot sprites - /// (same lookup GameWindow.OnLoad performs for the production binding). - public static IRetainedPanelController? Populate( - uint layoutId, - ImportedLayout layout, - RenderStack stack, - ClientObjectTable objects, - IDatReaderWriter dats) - { - switch (layoutId) - { - case 0x2100006Cu: // vitals - VitalsController.Bind(layout, - healthPct: () => SampleData.HealthPct, - staminaPct: () => SampleData.StaminaPct, - manaPct: () => SampleData.ManaPct, - healthText: () => "80/100", - staminaText: () => "60/100", - manaText: () => "90/100"); - return null; - - case RadarController.LayoutId: // gmRadarUI - return RadarController.Bind( - layout, - () => new UiRadarSnapshot( - PlayerHeadingDegrees: 32f, - Blips: - [ - new UiRadarBlip(0x50000001u, "Portal to Holtburg", 82f, 43f, - RadarColor(AcDream.Core.Ui.RadarBlipColors.Portal), - AcDream.Core.Ui.RadarBlipShape.Plus), - new UiRadarBlip(0x50000002u, "Drudge Prowler", 43f, 76f, - RadarColor(AcDream.Core.Ui.RadarBlipColors.Creature), - AcDream.Core.Ui.RadarBlipShape.Plus), - new UiRadarBlip(0x50000003u, "Town Crier", 68f, 87f, - RadarColor(AcDream.Core.Ui.RadarBlipColors.NPC), - AcDream.Core.Ui.RadarBlipShape.Box), - new UiRadarBlip(0x50000004u, "Selected PK", 39f, 39f, - RadarColor(AcDream.Core.Ui.RadarBlipColors.PlayerKiller), - AcDream.Core.Ui.RadarBlipShape.X, - Selected: true), - ], - CoordinatesText: "42.1N,33.3E"), - datFont: stack.VitalsDatFont); - - case CombatUiController.LayoutId: - { - var combat = new CombatState(); - var attacks = new RuntimeCombatAttackState( - combat, - canStartAttack: () => true, - sendAttack: (_, _) => true, - autoRepeatAttack: () => false); - GameplaySettings gameplay = GameplaySettings.Default; - CombatUiController? controller = CombatUiController.Bind( - layout, - combat, - attacks, - () => gameplay, - value => gameplay = value, - new CombatUiLabels( - "Speed", "Power", "Repeat Attacks", "Auto Target", "Keep in View", - "High", "Medium", "Low"), - visible => layout.Root.Visible = visible); - combat.SetCombatMode(CombatMode.Melee); - return controller; - } - - case 0x21000016u: // toolbar - return ToolbarController.Bind( - layout, - objects, - shortcuts: new ShortcutStore(), - iconIds: MakeIconIds(stack), - useItem: _ => { }, - combatState: null, - regularDigits: null, - ghostedDigits: null, - emptyDigits: null, - sendAddShortcut: null, - sendRemoveShortcut: null); - - case 0x21000023u: // inventory + paperdoll - { - // Resolve the per-list empty-slot art from the dat cell template, matching the - // exact lookup GameWindow.OnLoad performs (UIElement_ItemList::InternalCreateItem - // 0x004e3570 → attr 0x1000000e → catalog 0x21000037 → ItemSlot_Empty). - uint contentsEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000021u, 0x100001C6u); - uint sideBagEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000022u, 0x100001CAu); - uint mainPackEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000022u, 0x100001C9u); - var paperdollEmpty = PaperdollSlotBackgrounds.ResolveEmptySprites(dats); - - var iconIds = MakeIconIds(stack); - var selection = new AcDream.Core.Selection.SelectionState(); - var itemInteraction = new ItemInteractionController( - objects, - new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState( - new InventoryTransactionState(objects)), - new AcDream.Runtime.Gameplay.InteractionState(), - () => SampleData.PlayerGuid, - sendUse: null, - sendUseWithTarget: null, - sendWield: null, - sendDrop: null); - - InventoryController inventory = InventoryController.Bind( - layout, - objects, - playerGuid: () => SampleData.PlayerGuid, - iconIds: iconIds, - strength: () => 100, - selection: selection, - datFont: stack.VitalsDatFont, - contentsEmptySprite: contentsEmpty, - sideBagEmptySprite: sideBagEmpty, - mainPackEmptySprite: mainPackEmpty); - - // Bind the paperdoll equip slots with their authored per-location UIItem prototypes. - PaperdollController paperdoll = PaperdollController.Bind( - layout, - objects, - playerGuid: () => SampleData.PlayerGuid, - iconIds: iconIds, - selection: selection, - itemInteraction: itemInteraction, - emptySlotSprite: contentsEmpty, - datFont: stack.VitalsDatFont, - emptySlotSprites: paperdollEmpty, - ownsItemInteraction: true); - return new RetainedPanelControllerGroup(inventory, paperdoll); - } - - case 0x2100002Eu: // gmStatManagementUI — Attributes/Skills/Titles window (LayoutDesc 0x2100002E) - // Bind the REAL importer-mounted header + list elements (name/heritage/PK/level/ - // total-XP/XP-meter + the 9-row attribute list + footer State-A). NOT the text-report - // sub-panel (that is gmCharacterInfoUI 0x2100001A → CharacterController). - // LargeDatFont (0x40000001, MaxCharHeight=18) is used for the attribute row text; - // fallback to VitalsDatFont (0x40000000, 16px) if unavailable. - CharacterStatController.Bind( - layout, - data: SampleData.SampleCharacter, - datFont: stack.VitalsDatFont, - rowDatFont: stack.LargeDatFont ?? stack.VitalsDatFont, - spriteResolve: stack.ResolveChrome); - return null; - - default: - // Unknown layout — no-op; the panel renders structurally. - return null; - } - } - - // ── Helpers ───────────────────────────────────────────────────────────── - - /// - /// Build the iconIds delegate for toolbar / inventory controllers. - /// - /// - /// Raw-resolve stub: resolve the base (arg 2) - /// via and return its GL handle. - /// The remaining args (type, underlayId, overlayId, effects) are ignored - /// for the studio — a single-layer icon is sufficient for layout preview. - /// - /// - /// This is what the task spec calls "v1 raw-resolve stub". - /// - private static Func MakeIconIds(RenderStack stack) - => (_, iconId, _, _, _) => - { - if (iconId == 0u) return 0u; - var (handle, _, _) = stack.ResolveChrome(iconId); - return handle; - }; - - private static System.Numerics.Vector4 RadarColor(AcDream.Core.Ui.RadarBlipColors.Rgba color) - => new(color.Red, color.Green, color.Blue, color.Alpha); - -} diff --git a/src/AcDream.App/Studio/LayoutSource.cs b/src/AcDream.App/Studio/LayoutSource.cs deleted file mode 100644 index 78e53383..00000000 --- a/src/AcDream.App/Studio/LayoutSource.cs +++ /dev/null @@ -1,124 +0,0 @@ -using AcDream.App.UI; -using AcDream.App.UI.Layout; -using DatReaderWriter; -using AcDream.Content; - -namespace AcDream.App.Studio; - -/// Which kind of source the studio is currently previewing. -public enum LayoutSourceKind { DatLayout, Markup } - -/// -/// Wraps the two ways the UI Studio can load a panel to preview: -/// a LayoutDesc dat id, or a KSML markup file path (Task 6 — unsupported now). -/// -/// Call with the current to -/// import the layout and get the root . The result is also -/// cached in so can re-run the same -/// source without re-reading the options. -/// -public sealed class LayoutSource -{ - private readonly IDatReaderWriter _dats; - private readonly Func _resolve; - private readonly UiDatFont? _datFont; - private readonly Func? _fontResolve; - - public LayoutSourceKind Kind { get; private set; } - public uint? LayoutId { get; private set; } - public string? MarkupPath { get; private set; } - public string? LastError { get; private set; } - public ImportedLayout? CurrentLayout { get; private set; } - - /// - /// Create a LayoutSource. - /// - /// Optional per-element font resolver: FontDid → - /// (null when the font isn't in the dats). When supplied, - /// elements with a non-zero FontDid receive their own dat font at build time - /// instead of the shared global. Controllers that - /// explicitly set after - /// still override the build-time value. - /// Pass null (default) for the original single-font behavior — the live - /// path passes null so it is provably unchanged. - public LayoutSource( - IDatReaderWriter dats, - Func resolve, - UiDatFont? datFont, - Func? fontResolve = null) - { - _dats = dats ?? throw new ArgumentNullException(nameof(dats)); - _resolve = resolve ?? throw new ArgumentNullException(nameof(resolve)); - _datFont = datFont; - _fontResolve = fontResolve; - } - - /// - /// Load the layout described by . For a dat layout - /// ( is non-null) calls - /// . For a markup path sets - /// and returns null (Task 6, not yet implemented). - /// - /// Returns the root on success, or null on failure - /// (check ). - /// - public UiElement? Load(StudioOptions opts) - { - LastError = null; - CurrentLayout = null; - - if (opts.MarkupPath is not null) - { - Kind = LayoutSourceKind.Markup; - MarkupPath = opts.MarkupPath; - LastError = "markup unsupported (Task 6)"; - return null; - } - - if (opts.LayoutId is null) - { - LastError = "ui-studio: no layout id or markup path specified."; - return null; - } - - Kind = LayoutSourceKind.DatLayout; - LayoutId = opts.LayoutId; - return LoadDat(opts.LayoutId.Value); - } - - /// Re-run the most-recently-configured source without re-reading options. - public UiElement? Reload() - { - LastError = null; - CurrentLayout = null; - - if (Kind == LayoutSourceKind.Markup) - { - LastError = "markup unsupported (Task 6)"; - return null; - } - - if (LayoutId is null) - { - LastError = "ui-studio: no layout id to reload."; - return null; - } - - return LoadDat(LayoutId.Value); - } - - // ── Private ────────────────────────────────────────────────────────────────── - - private UiElement? LoadDat(uint layoutId) - { - var imported = LayoutImporter.Import(_dats, layoutId, _resolve, _datFont, _fontResolve); - if (imported is null) - { - LastError = $"ui-studio: LayoutDesc 0x{layoutId:X8} not found in dats."; - return null; - } - - CurrentLayout = imported; - return imported.Root; - } -} diff --git a/src/AcDream.App/Studio/MockupDesktop.cs b/src/AcDream.App/Studio/MockupDesktop.cs deleted file mode 100644 index eaa379f8..00000000 --- a/src/AcDream.App/Studio/MockupDesktop.cs +++ /dev/null @@ -1,493 +0,0 @@ -using AcDream.App.Rendering; -using AcDream.App.UI; -using AcDream.App.UI.Layout; -using AcDream.Core.Chat; -using AcDream.UI.Abstractions; -using AcDream.UI.Abstractions.Panels.Chat; -using DatReaderWriter; -using AcDream.Content; -using System.Numerics; - -namespace AcDream.App.Studio; - -/// -/// Builds a multi-window UI desktop for the studio. It mounts several real -/// imported panels into one production so drag, resize, -/// click, focus, and z-order behavior all run through the same UI root as the -/// game. -/// -internal static class MockupDesktop -{ - public static void Load(IDatReaderWriter dats, RenderStack stack) - { - var objects = SampleData.BuildObjectTable(); - var windows = new List(); - - if (MountVitals(dats, stack, objects) is { } vitals) - RecordWindow(windows, "Vitals", vitals, zOrder: 10); - if (MountToolbar(dats, stack, objects) is { } toolbar) - RecordWindow(windows, "Toolbar", toolbar, zOrder: 20); - if (MountCharacter(dats, stack, objects) is { } character) - RecordWindow(windows, "Character", character, zOrder: 30); - if (MountInventory(dats, stack, objects) is { } inventory) - RecordWindow(windows, "Inventory", inventory, zOrder: 40); - if (MountChat(dats, stack) is { } chat) - RecordWindow(windows, "Chat", chat, zOrder: 50); - - MountControls(stack, windows); - } - - private static ImportedLayout? Import(IDatReaderWriter dats, uint layoutId, RenderStack stack) - => LayoutImporter.Import(dats, layoutId, stack.ResolveChrome, stack.VitalsDatFont, - fontResolve: stack.ResolveDatFont); - - private static RetailWindowHandle? MountVitals(IDatReaderWriter dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects) - { - var layout = Import(dats, 0x2100006Cu, stack); - if (layout is null) return null; - - IRetainedPanelController? controller = - FixtureProvider.Populate(0x2100006Cu, layout, stack, objects, dats); - - return RetailWindowFrame.Mount( - stack.UiHost.Root, - layout.Root, - stack.ResolveChrome, - new RetailWindowFrame.Options - { - WindowName = WindowNames.Vitals, - Chrome = RetailWindowChrome.Imported, - Left = 12f, - Top = 18f, - ResizeX = true, - ResizeY = false, - MinWidth = 40f, - ContentClickThrough = false, - Controller = controller, - }); - } - - private static RetailWindowHandle? MountToolbar(IDatReaderWriter dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects) - { - var layout = Import(dats, 0x21000016u, stack); - if (layout is null) return null; - - IRetainedPanelController? controller = - FixtureProvider.Populate(0x21000016u, layout, stack, objects, dats); - - const int border = RetailChromeSprites.Border; - var toolbarRoot = layout.Root; - float contentW = toolbarRoot.Width > 0f ? toolbarRoot.Width : 300f; - float contentH = toolbarRoot.Height; - RetailWindowHandle handle = RetailWindowFrame.Mount( - stack.UiHost.Root, - toolbarRoot, - stack.ResolveChrome, - new RetailWindowFrame.Options - { - WindowName = WindowNames.Toolbar, - Chrome = RetailWindowChrome.CollapsibleNineSlice, - Left = 12f, - Top = 220f, - ContentWidth = contentW, - ContentHeight = contentH, - Resizable = false, - ResizeX = false, - ResizeY = true, - ResizableEdges = ResizeEdges.Bottom, - ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right, - ContentClickThrough = true, - Controller = controller, - }); - var frame = (UiCollapsibleFrame)handle.OuterFrame; - - ConfigureToolbarCollapse(layout, frame, contentH, border); - return handle; - } - - private static void ConfigureToolbarCollapse(ImportedLayout layout, UiCollapsibleFrame frame, float contentH, int border) - { - uint[] row2Ids = - { - 0x100006B6u, - 0x100006B7u, 0x100006B8u, 0x100006B9u, 0x100006BAu, 0x100006BBu, - 0x100006BCu, 0x100006BDu, 0x100006BEu, 0x100006BFu, - 0x100006C0u, - }; - - var row2 = new List(); - float minRow2Top = float.MaxValue; - foreach (var id in row2Ids) - { - if (layout.FindElement(id) is not { } element) continue; - row2.Add(element); - if (element.Top < minRow2Top) minRow2Top = element.Top; - } - - if (row2.Count == 0) return; - - float expandedH = contentH + 2 * border; - float collapsedH = minRow2Top + 2 * border; - frame.CollapsedHeight = collapsedH; - frame.ExpandedHeight = expandedH; - frame.SecondRow = row2; - frame.Resizable = true; - frame.ResizableEdges = ResizeEdges.Bottom; - frame.MinHeight = collapsedH; - frame.MaxHeight = expandedH; - } - - private static RetailWindowHandle? MountCharacter(IDatReaderWriter dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects) - { - var layout = Import(dats, 0x2100002Eu, stack); - if (layout is null) return null; - - IRetainedPanelController? controller = - FixtureProvider.Populate(0x2100002Eu, layout, stack, objects, dats); - - return RetailWindowFrame.Mount( - stack.UiHost.Root, - layout.Root, - stack.ResolveChrome, - new RetailWindowFrame.Options - { - WindowName = WindowNames.Character, - Chrome = RetailWindowChrome.NineSlice, - Left = 540f, - Top = 18f, - MaxHeight = 760f, - ResizeX = false, - ResizeY = true, - ResizableEdges = ResizeEdges.Bottom, - ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom, - ContentClickThrough = false, - Controller = controller, - }); - } - - private static RetailWindowHandle? MountInventory(IDatReaderWriter dats, RenderStack stack, AcDream.Core.Items.ClientObjectTable objects) - { - var layout = Import(dats, 0x21000023u, stack); - if (layout is null) return null; - - IRetainedPanelController? controller = - FixtureProvider.Populate(0x21000023u, layout, stack, objects, dats); - - var inventoryRoot = layout.Root; - float contentW = inventoryRoot.Width; - float contentH = inventoryRoot.Height; - RetailWindowHandle handle = RetailWindowFrame.Mount( - stack.UiHost.Root, - inventoryRoot, - stack.ResolveChrome, - new RetailWindowFrame.Options - { - WindowName = WindowNames.Inventory, - Chrome = RetailWindowChrome.NineSlice, - Left = 900f, - Top = 18f, - ContentWidth = contentW, - ContentHeight = contentH, - ResizeX = false, - ResizeY = true, - ResizableEdges = ResizeEdges.Bottom, - MaxHeight = Math.Max( - contentH + 2f * RetailChromeSprites.Border, - stack.UiHost.Root.Height - 18f), - ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom, - Controller = controller, - }); - - StretchV(layout, 0x100001D0u); - StretchV(layout, 0x100001CFu); - StretchV(layout, 0x100001C6u); - StretchV(layout, 0x100001C7u); - PinTopLeft(layout, 0x100001CDu); - PinTopLeft(layout, 0x100001CEu); - - return handle; - } - - private static void StretchV(ImportedLayout layout, uint id) - { - if (layout.FindElement(id) is { } element) - element.Anchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom; - } - - private static void PinTopLeft(ImportedLayout layout, uint id) - { - if (layout.FindElement(id) is { } element) - element.Anchors = AnchorEdges.Left | AnchorEdges.Top; - } - - private static RetailWindowHandle? MountChat(IDatReaderWriter dats, RenderStack stack) - { - var rootInfo = LayoutImporter.ImportInfos(dats, ChatWindowController.LayoutId); - if (rootInfo is null) return null; - - var strings = new DatStringResolver(dats); - var layout = LayoutImporter.Build(rootInfo, stack.ResolveChrome, stack.VitalsDatFont, - fontResolve: stack.ResolveDatFont, - stringResolve: strings.Resolve); - - var chatLog = new ChatLog(); - chatLog.SetLocalPlayerGuid(SampleData.PlayerGuid); - chatLog.OnSystemMessage("Welcome to the acdream UI mockup.", 0); - chatLog.OnLocalSpeech("You", "This desktop is running through UiHost.", SampleData.PlayerGuid, isRanged: false); - chatLog.OnChannelBroadcast(1, "Caith", "Drag and resize the windows here.", "General"); - chatLog.OnTellReceived("Alicia", "The old UI is starting to wake up.", 0x5000ABCDu); - - var vm = new ChatVM(chatLog, displayLimit: 200); - var controller = ChatWindowController.Bind( - rootInfo, - layout, - vm, - () => NullCommandBus.Instance, - stack.VitalsDatFont, - debugFont: null, - stack.ResolveChrome); - if (controller is null) return null; - - var chatRoot = controller.Root; - RetailWindowHandle handle = RetailWindowFrame.Mount( - stack.UiHost.Root, - chatRoot, - stack.ResolveChrome, - new RetailWindowFrame.Options - { - WindowName = WindowNames.Chat, - Chrome = RetailWindowChrome.NineSlice, - Left = 12f, - Top = 390f, - ContentWidth = 490f, - ContentHeight = chatRoot.Height, - RebaseContentLayout = true, - DatConstraintSource = controller.DatWindowInfo, - MinWidth = 200f, - ResizeX = true, - ResizeY = true, - Opacity = 0.75f, - StateController = controller, - }); - controller.AttachWindow(handle); - - stack.UiHost.Root.DefaultTextInput = controller.Input; - return handle; - } - - private static void RecordWindow( - List windows, - string title, - RetailWindowHandle handle, - int zOrder) - { - UiElement element = handle.OuterFrame; - element.ZOrder = zOrder; - windows.Add(new MockupWindow( - handle.Name, - title, - element, - new MockupRect(element.Left, element.Top, element.Width, element.Height), - zOrder)); - } - - private static void MountControls(RenderStack stack, IReadOnlyList windows) - { - if (windows.Count == 0) return; - - var font = stack.VitalsDatFont; - var panel = new MockupControlPanel - { - Name = "mockup-controls", - Left = 1010f, - Top = 430f, - Width = 258f, - Height = 242f, - Anchors = AnchorEdges.None, - Draggable = true, - Resizable = false, - BackgroundColor = new Vector4(0.03f, 0.035f, 0.04f, 0.82f), - BorderColor = new Vector4(0.55f, 0.45f, 0.27f, 0.95f), - BorderThickness = 1f, - ZOrder = 1_000, - }; - - panel.AddChild(new MockupLabel(font) - { - Left = 12f, - Top = 10f, - Width = 220f, - Height = 18f, - Text = "Mockup", - TextColor = new Vector4(0.95f, 0.86f, 0.62f, 1f), - }); - - var toggles = new List<(MockupWindow Window, MockupButton Button)>(); - panel.AddChild(MakeButton(font, "Arrange", 12f, 34f, 108f, 24f, () => - { - Arrange(windows, showAll: false); - RefreshToggles(toggles); - })); - panel.AddChild(MakeButton(font, "Reset", 132f, 34f, 108f, 24f, () => - { - Arrange(windows, showAll: true); - RefreshToggles(toggles); - })); - - float y = 70f; - foreach (var window in windows) - { - var button = MakeButton(font, "", 12f, y, 228f, 24f, () => - { - stack.UiHost.ToggleWindow(window.Name); - RefreshToggles(toggles); - }); - toggles.Add((window, button)); - panel.AddChild(button); - y += 30f; - } - - RefreshToggles(toggles); - stack.UiHost.Root.AddChild(panel); - } - - private static MockupButton MakeButton(UiDatFont? font, string text, - float left, float top, float width, float height, Action onClick) - { - var button = new MockupButton(font) - { - Left = left, - Top = top, - Width = width, - Height = height, - Text = text, - }; - button.Click += onClick; - return button; - } - - private static void Arrange(IReadOnlyList windows, bool showAll) - { - foreach (var window in windows) - { - var element = window.Element; - element.Left = window.DefaultRect.Left; - element.Top = window.DefaultRect.Top; - element.Width = window.DefaultRect.Width; - element.Height = window.DefaultRect.Height; - element.ZOrder = window.DefaultZOrder; - if (showAll) - element.Visible = true; - } - } - - private static void RefreshToggles(IReadOnlyList<(MockupWindow Window, MockupButton Button)> toggles) - { - foreach (var (window, button) in toggles) - { - bool visible = window.Element.Visible; - button.Text = visible ? $"{window.Title} on" : $"{window.Title} off"; - button.TextColor = visible - ? new Vector4(0.96f, 0.93f, 0.82f, 1f) - : new Vector4(0.62f, 0.62f, 0.62f, 1f); - button.BackgroundColor = visible - ? new Vector4(0.14f, 0.13f, 0.10f, 0.94f) - : new Vector4(0.06f, 0.065f, 0.07f, 0.82f); - } - } - - private sealed record MockupWindow( - string Name, - string Title, - UiElement Element, - MockupRect DefaultRect, - int DefaultZOrder); - - private readonly record struct MockupRect(float Left, float Top, float Width, float Height); - - private sealed class MockupControlPanel : UiPanel - { - protected override void OnDraw(UiRenderContext ctx) - { - if (BackgroundColor.W > 0f) - ctx.DrawFill(0f, 0f, Width, Height, BackgroundColor); - if (BorderColor.W > 0f && BorderThickness > 0f) - ctx.DrawRectOutline(0f, 0f, Width, Height, BorderColor, BorderThickness); - } - - protected override void OnTick(double deltaSeconds) - { - base.OnTick(deltaSeconds); - if (Parent is null) return; - - int top = ZOrder; - foreach (var sibling in Parent.Children) - { - if (!ReferenceEquals(sibling, this)) - top = Math.Max(top, sibling.ZOrder + 1); - } - ZOrder = top; - } - } - - private sealed class MockupLabel : UiElement - { - private readonly UiDatFont? _font; - - public string Text { get; set; } = string.Empty; - public Vector4 TextColor { get; set; } = Vector4.One; - - public MockupLabel(UiDatFont? font) - { - _font = font; - ClickThrough = true; - } - - protected override void OnDraw(UiRenderContext ctx) - { - if (_font is null || Text.Length == 0) return; - ctx.DrawStringDat(_font, Text, 0f, 0f, TextColor); - } - } - - private sealed class MockupButton : UiPanel - { - private readonly UiDatFont? _font; - - public event Action? Click; - public string Text { get; set; } = string.Empty; - public Vector4 TextColor { get; set; } = new(0.96f, 0.93f, 0.82f, 1f); - - public MockupButton(UiDatFont? font) - { - _font = font; - BackgroundColor = new Vector4(0.14f, 0.13f, 0.10f, 0.94f); - BorderColor = new Vector4(0.45f, 0.38f, 0.24f, 1f); - BorderThickness = 1f; - } - - public override bool HandlesClick => true; - - public override bool OnEvent(in UiEvent e) - { - if (e.Type != UiEventType.Click || !Enabled) return false; - Click?.Invoke(); - return true; - } - - protected override void OnDraw(UiRenderContext ctx) - { - if (BackgroundColor.W > 0f) - ctx.DrawFill(0f, 0f, Width, Height, BackgroundColor); - if (BorderColor.W > 0f && BorderThickness > 0f) - ctx.DrawRectOutline(0f, 0f, Width, Height, BorderColor, BorderThickness); - - if (_font is null || Text.Length == 0) return; - - float textW = _font.MeasureWidth(Text); - float tx = MathF.Max(4f, (Width - textW) * 0.5f); - float ty = (Height - _font.LineHeight) * 0.5f; - ctx.DrawStringDat(_font, Text, tx, ty, TextColor); - } - } -} diff --git a/src/AcDream.App/Studio/PanelFbo.cs b/src/AcDream.App/Studio/PanelFbo.cs deleted file mode 100644 index 4f61ec55..00000000 --- a/src/AcDream.App/Studio/PanelFbo.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System; -using System.Numerics; -using AcDream.App.Rendering.Wb; -using AcDream.App.UI; -using Silk.NET.OpenGL; - -namespace AcDream.App.Studio; - -/// -/// Renders a into an off-screen FBO each frame and -/// returns the color texture handle for display in ImGui. -/// -/// Pattern lifted verbatim from : -/// RGBA8 color texture + Depth24Stencil8 renderbuffer, lazily (re)created on -/// size change. The entire 2-D UI pass is sealed in a -/// so it cannot disturb the surrounding ImGui GL state. -/// -/// FBO origin is bottom-left (GL convention). The caller must flip V when -/// displaying the texture in ImGui (pass uv0=(0,1), uv1=(1,0) to ImGui.Image) -/// so the image appears right-side-up in ImGui's top-left coordinate system. -/// -public sealed unsafe class PanelFbo : IDisposable -{ - private readonly GL _gl; - - // Off-screen target — lazily (re)created when the requested size changes. - private uint _fbo; - private uint _colorTex; - private uint _depthRbo; - private int _fbW; - private int _fbH; - - public PanelFbo(GL gl) - { - _gl = gl ?? throw new ArgumentNullException(nameof(gl)); - } - - /// - /// Render (a full draw pass) into a - /// private FBO at × pixels. - /// Returns the GL color texture handle (0 on failure). The texture is valid until - /// the next call to with a different size, or until . - /// - public uint Render(int width, int height, UiHost host) - { - if (width <= 0 || height <= 0 || host is null) return 0u; - - EnsureFramebuffer(width, height); - if (_fbo == 0) return 0u; - - // Seal the entire pass: GLStateScope saves + restores every GL state the - // UI draw touches (viewport, blend, FBO binding, etc.) so ImGui's own state - // — set up by BeginFrame and expected intact by Render — is untouched. - using var scope = new GLStateScope(_gl); - - _gl.BindFramebuffer(FramebufferTarget.Framebuffer, _fbo); - _gl.Viewport(0, 0, (uint)width, (uint)height); - _gl.Disable(EnableCap.ScissorTest); - _gl.ClearColor(0.18f, 0.18f, 0.18f, 1f); // opaque dark-grey canvas background (the FBO IS the canvas) - _gl.ClearDepth(1.0); - _gl.DepthMask(true); - _gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); - - host.Draw(new Vector2(width, height)); - - // FBO stays bound here; GLStateScope.Dispose() restores the previous binding. - return _colorTex; - } - - /// - /// Read the FBO color attachment back to CPU as a flat RGBA8 byte array. - /// Must be called AFTER for the same and - /// (so the FBO exists and is the right size). - /// - /// FBO origin is bottom-left (GL convention). The caller is responsible for - /// flipping rows vertically before saving as a top-left-origin image format (PNG). - /// - /// Returns an empty array when the FBO is not ready. - /// - public unsafe byte[] ReadColorRgba(int width, int height) - { - if (_fbo == 0 || width <= 0 || height <= 0) return Array.Empty(); - - int byteCount = width * height * 4; - var buf = new byte[byteCount]; - - _gl.BindFramebuffer(FramebufferTarget.Framebuffer, _fbo); - fixed (byte* p = buf) - { - _gl.ReadPixels(0, 0, (uint)width, (uint)height, - PixelFormat.Rgba, PixelType.UnsignedByte, p); - } - _gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0); - - return buf; - } - - // ── FBO lifecycle (mirrors PaperdollViewportRenderer.EnsureFramebuffer) ────── - - private void EnsureFramebuffer(int width, int height) - { - if (_fbo != 0 && width == _fbW && height == _fbH) return; - DeleteFramebuffer(); - - _fbW = width; - _fbH = height; - - _colorTex = _gl.GenTexture(); - _gl.BindTexture(TextureTarget.Texture2D, _colorTex); - _gl.TexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Rgba8, - (uint)width, (uint)height, 0, - PixelFormat.Rgba, PixelType.UnsignedByte, (void*)0); - _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, - (int)TextureMinFilter.Linear); - _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, - (int)TextureMinFilter.Linear); - _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, - (int)TextureWrapMode.ClampToEdge); - _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, - (int)TextureWrapMode.ClampToEdge); - _gl.BindTexture(TextureTarget.Texture2D, 0); - - _depthRbo = _gl.GenRenderbuffer(); - _gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, _depthRbo); - _gl.RenderbufferStorage(RenderbufferTarget.Renderbuffer, - InternalFormat.Depth24Stencil8, (uint)width, (uint)height); - _gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, 0); - - _fbo = _gl.GenFramebuffer(); - _gl.BindFramebuffer(FramebufferTarget.Framebuffer, _fbo); - _gl.FramebufferTexture2D(FramebufferTarget.Framebuffer, - FramebufferAttachment.ColorAttachment0, - TextureTarget.Texture2D, _colorTex, 0); - _gl.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, - FramebufferAttachment.DepthStencilAttachment, - RenderbufferTarget.Renderbuffer, _depthRbo); - - var status = _gl.CheckFramebufferStatus(FramebufferTarget.Framebuffer); - _gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0); - if (status != GLEnum.FramebufferComplete) - { - Console.WriteLine($"[studio] PanelFbo incomplete: {status} ({width}x{height})"); - DeleteFramebuffer(); - } - } - - private void DeleteFramebuffer() - { - if (_fbo != 0) { _gl.DeleteFramebuffer(_fbo); _fbo = 0; } - if (_colorTex != 0) { _gl.DeleteTexture(_colorTex); _colorTex = 0; } - if (_depthRbo != 0) { _gl.DeleteRenderbuffer(_depthRbo); _depthRbo = 0; } - _fbW = _fbH = 0; - } - - public void Dispose() => DeleteFramebuffer(); -} diff --git a/src/AcDream.App/Studio/SampleData.cs b/src/AcDream.App/Studio/SampleData.cs deleted file mode 100644 index e523fc8e..00000000 --- a/src/AcDream.App/Studio/SampleData.cs +++ /dev/null @@ -1,261 +0,0 @@ -using AcDream.App.UI.Layout; -using AcDream.Core.Items; - -namespace AcDream.App.Studio; - -/// -/// Static sample data for the UI Studio fixture provider. -/// Provides a pre-built populated with a -/// representative player, their main-pack items, side bags, and equipped gear, -/// so the 2-D inventory / paperdoll panels render populated when previewed in -/// the studio without a live game session. -/// -/// Icon ids used (all real 0x06xxxxxx RenderSurface ids confirmed in the dats): -/// Inventory empty-slot sprite : 0x06004D20 -/// Generic "misc" item : 0x060011D4 (icon_misc_underlay / fallback) -/// Iron Sword (melee weapon) : 0x060011CBu (acclient default weapon underlay) -/// Leather Breastplate (armor) : 0x060011CFu (acclient default armor underlay) -/// Leather Gloves : 0x060011F3u (acclient default clothing underlay) -/// Steel Ring : 0x060011D5u (acclient default jewelry underlay) -/// Healing Kit : 0x060011D4u (generic misc fallback) -/// Spell components : 0x060011D4u (generic misc fallback) -/// Side-bag 1 (Container) : 0x06004D20u -/// Side-bag 2 (Container) : 0x06004D20u -/// Equipped helm (HeadWear) : 0x060011F3u -/// Equipped chest armor : 0x060011CFu -/// Equipped melee weapon : 0x060011CBu -/// -/// These are the icon *base* RenderSurface ids — the same ids GameWindow passes -/// as `iconId` into the iconIds lambda. FixtureProvider resolves them via -/// and returns the raw GL handle. -/// -public static class SampleData -{ - // ── Guids ──────────────────────────────────────────────────────────────── - - /// Fake server guid for the studio's synthetic player. - public const uint PlayerGuid = 0x50000001u; - - // Items in main pack (slots 0–5). - private const uint SwordGuid = 0x50000010u; - private const uint ChestGuid = 0x50000011u; - private const uint GlovesGuid = 0x50000012u; - private const uint RingGuid = 0x50000013u; - private const uint HealKitGuid = 0x50000014u; - private const uint CompGuid = 0x50000015u; - - // Side bags (also in main pack, ContainerId = PlayerGuid; slots 6 & 7). - private const uint Bag1Guid = 0x50000020u; - private const uint Bag2Guid = 0x50000021u; - - // Equipped items (ContainerId = PlayerGuid, CurrentlyEquippedLocation set). - private const uint HelmGuid = 0x50000030u; - private const uint ChestEqGuid = 0x50000031u; - private const uint WeaponEqGuid = 0x50000032u; - - // ── Icon ids (0x06xxxxxx RenderSurface dat ids) ─────────────────────────── - - // These are the same underlay/fallback icon ids the IconComposer tests pin. - private const uint IconWeapon = 0x060011CBu; // weapon underlay - private const uint IconArmor = 0x060011CFu; // armor underlay - private const uint IconClothing = 0x060011F3u; // clothing underlay - private const uint IconJewelry = 0x060011D5u; // jewelry underlay - private const uint IconMisc = 0x060011D4u; // misc / fallback underlay - - // ── Public API ────────────────────────────────────────────────────────── - - /// - /// Build a fresh populated with the - /// studio's sample player + a realistic inventory snapshot. - /// The table is owned by the caller and should be kept alive for the - /// window's lifetime. - /// - public static ClientObjectTable BuildObjectTable() - { - var t = new ClientObjectTable(); - - // ── Player object ───────────────────────────────────────────────── - t.AddOrUpdate(new ClientObject - { - ObjectId = PlayerGuid, - Name = "Studio Player", - Type = ItemType.Creature, - ItemsCapacity = 102, - ContainersCapacity = 7, - }); - // Show all three authored sigil locations in inventory/paperdoll previews. - t.UpdateIntProperty( - PlayerGuid, - AetheriaUnlocks.PropertyId, - (int)AetheriaUnlockState.All); - - // ── Loose items in main pack (slots 0–5) ────────────────────────── - - AddItem(t, SwordGuid, ItemType.MeleeWeapon, IconWeapon, "Iron Sword", PlayerGuid, 0, burden: 60); - AddItem(t, ChestGuid, ItemType.Armor, IconArmor, "Leather Breastplate", PlayerGuid, 1, burden: 200); - AddItem(t, GlovesGuid, ItemType.Clothing, IconClothing, "Leather Gloves", PlayerGuid, 2, burden: 50); - AddItem(t, RingGuid, ItemType.Jewelry, IconJewelry, "Steel Ring", PlayerGuid, 3, burden: 10); - AddItem(t, HealKitGuid, ItemType.Misc, IconMisc, "Healing Kit", PlayerGuid, 4, burden: 30); - AddItem(t, CompGuid, ItemType.SpellComponents, IconMisc, "Spell Comps", PlayerGuid, 5, burden: 25, stackSize: 50, stackMax: 100); - - // ── Side bags (Container items in main pack, slots 6 & 7) ───────── - - AddItem(t, Bag1Guid, ItemType.Container, IconMisc, "Small Pack 1", - containerId: PlayerGuid, slot: 6, burden: 20, itemsCapacity: 24); - AddItem(t, Bag2Guid, ItemType.Container, IconMisc, "Small Pack 2", - containerId: PlayerGuid, slot: 7, burden: 20, itemsCapacity: 24); - - // ── Equipped items (ContainerId = PlayerGuid, CurrentlyEquippedLocation set) ── - - AddEquipped(t, HelmGuid, ItemType.Armor, IconClothing, "Tin Helm", EquipMask.HeadWear); - AddEquipped(t, ChestEqGuid, ItemType.Armor, IconArmor, "Chain Coat", EquipMask.ChestArmor); - AddEquipped(t, WeaponEqGuid, ItemType.MeleeWeapon, IconWeapon, "Wooden Sword", EquipMask.MeleeWeapon); - - return t; - } - - // ── Sample vital constants (used by FixtureProvider) ──────────────────── - - public const float HealthPct = 0.8f; - public const float StaminaPct = 0.6f; - public const float ManaPct = 0.9f; - - // ── Sample character sheet (used by CharacterController in the Studio) ─── - - /// - /// Returns a representative for the studio's - /// synthetic character. Values are plausible retail-scale numbers so the - /// report renders with well-proportioned text in all sections. - /// - public static CharacterSheet SampleCharacter() => SampleCharacter(null); - - public static CharacterSheet SampleCharacter(string? name) => new() - { - Name = string.IsNullOrWhiteSpace(name) ? "Studio Player" : name, - Level = 126, - Gender = "Female", - Heritage = "Aluvian", - Title = "the Adventurer", - BirthDate = "January 5, 2001", - PlayTime = "2 years, 114 days, 4 hours", - Deaths = 42, - - PkStatus = "Non-Player Killer", - TotalXp = 1_250_000_000, - XpToNextLevel = 42_000_000, - XpFraction = 0.63f, - - // Vitals: retail screenshot spec (Pass 1 acceptance criteria §Goal). - HealthCurrent = 5, HealthMax = 5, - StaminaCurrent = 10, StaminaMax = 10, - ManaCurrent = 10, ManaMax = 10, - - // Attributes: Strength + Quickness = 200; all others = 10 (retail screenshot spec §Goal). - Strength = 200, - Endurance = 10, - Quickness = 200, - Coordination = 10, - Focus = 10, - Self = 10, - - UnspentSkillCredits = 12, - SpecializedSkillCredits = 4, - ChessRank = 12, - FishingSkill = 4, - - // Available skill credits (retail InqInt(0x18)); shown in Attributes tab footer State-A. - SkillCredits = 96, - - // Unassigned (banked) XP (retail InqInt64(2)); footer State-A line-2 value. - UnassignedXp = 87_757_321_741L, - - // Raise costs in retail display order (Strength, Endurance, Coordination, Quickness, - // Focus, Self, Health, Stamina, Mana). - // Str@200 = maxed → 0 (disabled). Quickness@200 = maxed → 0. Others @10 → affordable. - // Focus@10 → 110 matches the authoritative retail screenshot (spec §4). - // Formula bracket at value=10: ExperienceToAttributeLevel(11) − ExperienceToAttributeLevel(10). - AttributeRaiseCosts = new long[] { 0L, 95L, 100L, 0L, 110L, 105L, 90L, 88L, 112L }, - AttributeRaise10Costs = new long[] { 0L, 950L, 1_000L, 0L, 1_100L, 1_050L, 900L, 880L, 1_120L }, - - // Real SkillTable icon IDs and train/specialize costs from client_portal.dat - // SkillTable 0x0E000004. gmSkillUI groups/sorts these at bind time. - Skills = new CharacterSkill[] - { - new( 6, "Melee Defense", 0x06000165u, CharacterSkillAdvancementClass.Specialized, 350, 354, false, 10, 20, 18_250_000L, 182_500_000L), - new(34, "War Magic", 0x06001365u, CharacterSkillAdvancementClass.Specialized, 280, 285, false, 16, 28, 11_100_000L, 111_000_000L), - - new(14, "Arcane Lore", 0x0600016Eu, CharacterSkillAdvancementClass.Trained, 260, 269, false, 4, 6, 7_500_000L, 75_000_000L), - new(33, "Life Magic", 0x06001364u, CharacterSkillAdvancementClass.Trained, 250, 252, false, 12, 20, 6_800_000L, 68_000_000L), - new(47, "Missile Weapons", 0x0600015Fu, CharacterSkillAdvancementClass.Trained, 220, 221, false, 6, 12, 5_250_000L, 52_500_000L), - - new(21, "Healing", 0x06000133u, CharacterSkillAdvancementClass.Untrained, 10, 10, true, 6, 10, 0L), - new(22, "Jump", 0x0600016Bu, CharacterSkillAdvancementClass.Untrained, 210, 210, true, 0, 4, 0L), - new(36, "Loyalty", 0x06001367u, CharacterSkillAdvancementClass.Untrained, 10, 10, true, 0, 2, 0L), - new(24, "Run", 0x06000173u, CharacterSkillAdvancementClass.Untrained, 390, 390, true, 0, 4, 0L), - - new(38, "Alchemy", 0x060019E4u, CharacterSkillAdvancementClass.Untrained, 10, 10, false, 6, 12, 0L), - new(39, "Cooking", 0x06001A54u, CharacterSkillAdvancementClass.Untrained, 10, 10, false, 4, 8, 0L), - new(37, "Fletching", 0x06001A55u, CharacterSkillAdvancementClass.Untrained, 10, 10, false, 4, 8, 0L), - }, - - CharacterInfoProperties = new Dictionary - { - [0x162u] = 2, // Swords melee mastery - }, - - BurdenCurrent = 1200, - BurdenMax = 4500, - }; - - // ── Helpers ───────────────────────────────────────────────────────────── - - private static void AddItem( - ClientObjectTable t, - uint guid, - ItemType type, - uint iconId, - string name, - uint containerId, - int slot, - int burden = 0, - int stackSize = 1, - int stackMax = 1, - int itemsCapacity = 0) - { - t.AddOrUpdate(new ClientObject - { - ObjectId = guid, - Name = name, - Type = type, - IconId = iconId, - Burden = burden, - StackSize = stackSize, - StackSizeMax = stackMax, - ItemsCapacity = itemsCapacity, - }); - t.MoveItem(guid, containerId, slot); - } - - private static void AddEquipped( - ClientObjectTable t, - uint guid, - ItemType type, - uint iconId, - string name, - EquipMask equipMask) - { - t.AddOrUpdate(new ClientObject - { - ObjectId = guid, - Name = name, - Type = type, - IconId = iconId, - ValidLocations = equipMask, - CurrentlyEquippedLocation = equipMask, - ContainerId = PlayerGuid, - }); - // Update the equip location on the object via MoveItem (sets ContainerId + - // CurrentlyEquippedLocation via the equip overload). - t.MoveItem(guid, PlayerGuid, newSlot: -1, newEquipLocation: equipMask); - } -} diff --git a/src/AcDream.App/Studio/StudioFrameCloseGate.cs b/src/AcDream.App/Studio/StudioFrameCloseGate.cs deleted file mode 100644 index 4450d3f4..00000000 --- a/src/AcDream.App/Studio/StudioFrameCloseGate.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace AcDream.App.Studio; - -/// -/// Defers a close requested by render work until the caller has completed the -/// active render-frame transaction. Silk dispatches Closing -/// synchronously, so closing directly from a render callback would otherwise -/// dispose GPU resources while submitted work is still owned by that frame. -/// -internal sealed class StudioFrameCloseGate -{ - private bool _requested; - - public bool IsRequested => _requested; - - public void Request() - { - _requested = true; - } - - public void CompleteFrame(Action close) - { - ArgumentNullException.ThrowIfNull(close); - - if (!_requested) - return; - - _requested = false; - close(); - } -} diff --git a/src/AcDream.App/Studio/StudioInspector.cs b/src/AcDream.App/Studio/StudioInspector.cs deleted file mode 100644 index 26e6dc0d..00000000 --- a/src/AcDream.App/Studio/StudioInspector.cs +++ /dev/null @@ -1,295 +0,0 @@ -using System.Numerics; -using AcDream.App.UI; -using ImGuiNET; - -namespace AcDream.App.Studio; - -/// -/// All canvas mouse events gathered by in one frame. -/// All coordinates are already mapped to panel-local pixels (origin top-left, same as UiRoot). -/// -public readonly struct CanvasInputEvent -{ - /// Mouse is currently hovering the canvas image. When false all other fields are 0 / false. - public readonly bool IsHovered; - /// Panel-local pixel coordinate of the mouse this frame (valid when ). - public readonly int MouseX; - /// Panel-local pixel coordinate of the mouse this frame (valid when ). - public readonly int MouseY; - /// Left-button went down this frame. - public readonly bool LeftDown; - /// Left-button came up this frame. - public readonly bool LeftUp; - /// Mouse-wheel scroll delta (lines, positive = up). Zero when no scroll. - public readonly int ScrollDelta; - - public CanvasInputEvent(bool hovered, int mx, int my, bool ld, bool lu, int scroll) - { - IsHovered = hovered; - MouseX = mx; - MouseY = my; - LeftDown = ld; - LeftUp = lu; - ScrollDelta = scroll; - } -} - -/// -/// Four-pane ImGui IDE for the acdream UI Studio: -/// -/// Toolbar — panel picker (slug combo) across the top. -/// Canvas — shows the panel FBO texture; in Interact mode mouse events -/// are forwarded to the panel UiHost (buttons/tabs respond); in Inspect mode a -/// left-click hit-tests and selects the element under the cursor. -/// Tree — recursive ImGui tree of the element hierarchy; clicking a node -/// sets . -/// Properties — shows the element's geometry, -/// anchors, and z-order. -/// -/// -/// Coordinate mapping for the canvas: -/// The FBO is rendered at the full window size and displayed 1:1 inside the Canvas ImGui -/// sub-window. After ImGui.Image we call ImGui.GetItemRectMin() to get the -/// screen-space top-left of the drawn image (accounting for the sub-window's title bar, -/// padding, and any scrolling). Subtracting that from the raw mouse screen position gives -/// panel-local pixels directly — no additional scale factor is needed because the image is -/// drawn 1:1. -/// -/// V-flip — no extra Y inversion needed: -/// The FBO origin is bottom-left (GL convention), so we pass uv0=(0,1), uv1=(1,0) to -/// ImGui.Image to flip V. After this flip, displayed row 0 (top of the image on -/// screen) corresponds to panel Y=0 (the top of the UI panel), matching UiRoot's -/// top-left origin. Therefore the panel-local Y computed above maps directly into UiRoot -/// without further inversion — do NOT flip Y again. -/// -/// Layout: the four panes call SetNextWindowPos + SetNextWindowSize -/// with ImGuiCond.FirstUseEver so they start docked but can be freely dragged. -/// -public sealed class StudioInspector -{ - /// Currently selected element (set by tree-click or canvas-click in Inspect mode). - public UiElement? Selected { get; set; } - - /// - /// When true (default) canvas mouse events are forwarded to the panel UiHost so elements - /// respond to clicks. When false a canvas click hit-tests and selects an element in the - /// inspector tree instead. Toggle via the "Interact / Inspect" checkbox in the toolbar. - /// - public bool InteractMode { get; set; } = true; - - // ── Toolbar ─────────────────────────────────────────────────────────────────── - - /// - /// Draw the "Studio" toolbar window (top strip) containing a slug combo-box and - /// the Interact / Inspect mode toggle. Returns the newly-selected slug when the - /// user picks a different panel, or null when unchanged. - /// The mode toggle sets : checked = Interact (panel - /// elements respond to clicks), unchecked = Inspect (clicks select elements in the - /// tree). - /// - /// All available panel slugs (from UiDumpModel.ListSlugs). - /// The slug of the panel currently loaded. - /// Studio window width (pixels). - public string? DrawToolbar(IReadOnlyList slugs, string? current, int windowW) - { - ImGui.SetNextWindowPos(new Vector2(0f, 0f), ImGuiCond.FirstUseEver); - ImGui.SetNextWindowSize(new Vector2(windowW, 40f), ImGuiCond.FirstUseEver); - ImGui.Begin("Studio", - ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse); - - ImGui.SetNextItemWidth(300f); - string? result = null; - string preview = current ?? "(none)"; - if (ImGui.BeginCombo("Panel", preview)) - { - foreach (var slug in slugs) - { - bool selected = string.Equals(slug, current, StringComparison.OrdinalIgnoreCase); - if (ImGui.Selectable(slug, selected) && !selected) - result = slug; - if (selected) - ImGui.SetItemDefaultFocus(); - } - ImGui.EndCombo(); - } - - ImGui.SameLine(); - bool interact = InteractMode; - if (ImGui.Checkbox("Interact", ref interact)) - InteractMode = interact; - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Interact: canvas clicks reach the panel (buttons/tabs respond).\nUncheck to Inspect: clicks select elements in the tree."); - - ImGui.End(); - return result; - } - - // ── Canvas ──────────────────────────────────────────────────────────────────── - - /// - /// Draw the "Canvas" ImGui window containing the panel FBO texture and return all - /// canvas mouse events for this frame as a . - /// - /// Coordinate mapping: After ImGui.Image, GetItemRectMin() - /// returns the actual screen-space top-left of the drawn image (accounting for the - /// sub-window title bar, padding, and scrolling). Subtracting that from the raw ImGui - /// mouse position gives panel-local pixels directly — no scale factor because the - /// image is drawn 1:1. - /// - /// V-flip — no extra Y inversion: we pass uv0=(0,1) / uv1=(1,0) so the - /// GL bottom-left origin is flipped to top-left on screen. After the flip, screen - /// row 0 = panel Y 0 (top of the UI), so the computed Y already matches UiRoot's - /// top-left origin — do NOT flip Y again. - /// - /// If is non-null a bright-green 2-pixel outline is - /// drawn over it using the window draw list. - /// - public CanvasInputEvent DrawCanvas(nint panelTex, int width, int height, - int windowX, int windowW, int windowY, int windowH) - { - ImGui.SetNextWindowPos(new Vector2(windowX, windowY), ImGuiCond.FirstUseEver); - ImGui.SetNextWindowSize(new Vector2(windowW, windowH), ImGuiCond.FirstUseEver); - ImGui.Begin("Canvas"); - - var imageSize = new Vector2(width, height); - // V-flip: FBO origin is bottom-left; ImGui images expect top-left. - // uv0 = bottom-left of texture = top of the panel in screen space. - // uv1 = top-right of texture = bottom of the panel in screen space. - var uv0 = new Vector2(0f, 1f); - var uv1 = new Vector2(1f, 0f); - ImGui.Image(panelTex, imageSize, uv0, uv1); - - // rectMin: screen-space top-left of the image AFTER ImGui.Image + any chrome offset. - // This is what lets us translate raw mouse screen coords into panel-local pixels. - var rectMin = ImGui.GetItemRectMin(); - - // ── Selection highlight ─────────────────────────────────────────────── - var el = Selected; - if (el is not null && el.Width > 0f && el.Height > 0f) - { - var sp = el.ScreenPosition; - var p0 = new Vector2(rectMin.X + sp.X, rectMin.Y + sp.Y); - var p1 = new Vector2(p0.X + el.Width, p0.Y + el.Height); - var dl = ImGui.GetWindowDrawList(); - dl.AddRect(p0, p1, - ImGui.GetColorU32(new Vector4(0.2f, 1f, 0.4f, 1f)), - 0f, ImDrawFlags.None, 2f); - } - - // ── Gather canvas mouse events ──────────────────────────────────────── - // IsItemHovered is true when the mouse is over the Image item (not just the window). - bool hovered = ImGui.IsItemHovered(); - int mx = 0, my = 0; - bool leftDown = false, leftUp = false; - int scroll = 0; - - if (hovered) - { - var mousePos = ImGui.GetMousePos(); - // Panel-local pixel = mouse offset from the image's screen-space top-left. - // Scale is 1:1 (image drawn at full FBO size). Y needs no extra flip — see summary. - int ix = (int)(mousePos.X - rectMin.X); - int iy = (int)(mousePos.Y - rectMin.Y); - // Clamp to image bounds (mouse can be on the image edge pixel). - if (ix >= 0 && ix < width && iy >= 0 && iy < height) - { - mx = ix; - my = iy; - leftDown = ImGui.IsMouseClicked(ImGuiMouseButton.Left); - leftUp = ImGui.IsMouseReleased(ImGuiMouseButton.Left); - float wheelY = ImGui.GetIO().MouseWheel; - scroll = (int)wheelY; // positive = scroll up - } - else - { - // Mouse is over ImGui chrome (title bar, padding) adjacent to image — not over the panel. - hovered = false; - } - } - - ImGui.End(); - return new CanvasInputEvent(hovered, mx, my, leftDown, leftUp, scroll); - } - - // ── Tree ────────────────────────────────────────────────────────────────────── - - /// Draw the "Tree" ImGui window. Clicking a node sets . - public void DrawTree(UiElement root, int windowX, int windowY, int windowW, int windowH) - { - ImGui.SetNextWindowPos(new Vector2(windowX, windowY), ImGuiCond.FirstUseEver); - ImGui.SetNextWindowSize(new Vector2(windowW, windowH), ImGuiCond.FirstUseEver); - ImGui.Begin("Tree"); - DrawTreeNode(root); - ImGui.End(); - } - - private void DrawTreeNode(UiElement el) - { - // Label: EventId (hex) + C# type name, e.g. "0x10000001 [UiDatElement]" - string label = $"0x{el.EventId:X8} [{el.GetType().Name}]"; - - bool isSelected = ReferenceEquals(el, Selected); - bool hasChildren = el.Children.Count > 0; - - ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags.OpenOnArrow - | ImGuiTreeNodeFlags.SpanAvailWidth; - if (!hasChildren) - flags |= ImGuiTreeNodeFlags.Leaf; - if (isSelected) - flags |= ImGuiTreeNodeFlags.Selected; - - bool open = ImGui.TreeNodeEx(label, flags); - - // Click on the node label (not the arrow) selects it. - if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) - Selected = el; - - if (open) - { - foreach (var child in el.Children) - DrawTreeNode(child); - ImGui.TreePop(); - } - } - - // ── Properties ─────────────────────────────────────────────────────────────── - - /// Draw the "Properties" ImGui window for . - public void DrawProperties(int windowX, int windowY, int windowW, int windowH) - { - ImGui.SetNextWindowPos(new Vector2(windowX, windowY), ImGuiCond.FirstUseEver); - ImGui.SetNextWindowSize(new Vector2(windowW, windowH), ImGuiCond.FirstUseEver); - ImGui.Begin("Properties"); - - var el = Selected; - if (el is null) - { - ImGui.TextUnformatted("(nothing selected)"); - ImGui.End(); - return; - } - - ImGui.TextUnformatted($"Id (EventId): 0x{el.EventId:X8}"); - ImGui.TextUnformatted($"Type: {el.GetType().Name}"); - ImGui.TextUnformatted($"Name: {el.Name ?? "(null)"}"); - ImGui.Separator(); - ImGui.TextUnformatted($"Rect: ({el.Left}, {el.Top}, {el.Width} x {el.Height})"); - ImGui.TextUnformatted($"Anchors: {el.Anchors}"); - ImGui.TextUnformatted($"ZOrder: {el.ZOrder}"); - ImGui.Separator(); - ImGui.TextUnformatted($"Visible: {el.Visible}"); - ImGui.TextUnformatted($"Enabled: {el.Enabled}"); - ImGui.TextUnformatted($"ClickThrough: {el.ClickThrough}"); - ImGui.TextUnformatted($"Draggable: {el.Draggable}"); - ImGui.TextUnformatted($"Resizable: {el.Resizable}"); - ImGui.TextUnformatted($"IsDragSource: {el.IsDragSource}"); - ImGui.TextUnformatted($"HandlesClick: {el.HandlesClick}"); - ImGui.TextUnformatted($"Opacity: {el.Opacity:F2}"); - ImGui.Separator(); - var sp = el.ScreenPosition; - ImGui.TextUnformatted($"ScreenPos: ({sp.X:F1}, {sp.Y:F1})"); - ImGui.TextUnformatted($"Children: {el.Children.Count}"); - - ImGui.End(); - } -} diff --git a/src/AcDream.App/Studio/StudioOptions.cs b/src/AcDream.App/Studio/StudioOptions.cs deleted file mode 100644 index 8e2a3c2e..00000000 --- a/src/AcDream.App/Studio/StudioOptions.cs +++ /dev/null @@ -1,143 +0,0 @@ -namespace AcDream.App.Studio; - -/// -/// Parsed options for the acdream UI Studio standalone tool. -/// Constructed by from the command-line tokens that follow -/// the ui-studio dispatch token. -/// -public sealed record StudioOptions( - string DatDir, - uint? LayoutId, - string? MarkupPath, - string? DumpSlug = null, - string? DumpFile = null, - string? ScreenshotPath = null, - bool Mockup = false, - string? CapabilityReportPath = null, - bool AudioSmoke = false) -{ - /// - /// Parse studio options from the args that come AFTER the ui-studio token. - /// - /// Positional (first non-flag arg): dat directory. Falls back to - /// ACDREAM_DAT_DIR when omitted. - /// --layout 0xNNNN: hex LayoutDesc dat id to preview. - /// --markup <path>: path to a KSML markup file (Task 6, unsupported for now). - /// --dump <slug>: load a panel from the retail UI dump JSON by slug - /// (e.g. inventory, radar, toolbar). Static mockup — no controllers. - /// --dump-file <path>: override the default dump file path - /// (docs/research/2026-06-25-retail-ui-layout-dump.json from the solution root). - /// Only meaningful when --dump is also given. - /// --screenshot <path>: headless mode — render the loaded panel to a PNG - /// at and exit without showing an interactive window. - /// Combines with --dump or --layout. - /// When neither --layout, --markup, nor --dump is given the - /// default layout 0x2100006C (vitals) is used. - /// - public static StudioOptions Parse(string[] args) - { - string? datDir = null; - uint? layoutId = null; - string? markupPath = null; - string? dumpSlug = null; - string? dumpFile = null; - string? screenshotPath = null; - string? capabilityReportPath = null; - bool mockup = false; - bool audioSmoke = false; - - for (int i = 0; i < args.Length; i++) - { - if (args[i] == "--layout" && i + 1 < args.Length) - { - var raw = args[++i]; - // Accept 0xNNNN or plain hex. - if (raw.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) - raw = raw[2..]; - if (uint.TryParse(raw, System.Globalization.NumberStyles.HexNumber, null, out var id)) - layoutId = id; - } - else if (args[i] == "--markup" && i + 1 < args.Length) - { - markupPath = args[++i]; - } - else if (args[i] == "--dump" && i + 1 < args.Length) - { - dumpSlug = args[++i]; - } - else if (args[i] == "--dump-file" && i + 1 < args.Length) - { - dumpFile = args[++i]; - } - else if (args[i] == "--screenshot" && i + 1 < args.Length) - { - screenshotPath = args[++i]; - } - else if (args[i] == "--capability-report" && i + 1 < args.Length) - { - capabilityReportPath = args[++i]; - } - else if (args[i] == "--audio-smoke") - { - audioSmoke = true; - } - else if (args[i] == "--mockup") - { - mockup = true; - } - else if (!args[i].StartsWith('-')) - { - datDir ??= args[i]; - } - } - - // Fall back to ACDREAM_DAT_DIR when no positional dat dir was given. - datDir ??= Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR"); - - if (string.IsNullOrWhiteSpace(datDir)) - throw new InvalidOperationException( - "ui-studio: dat directory required — pass as first arg or set ACDREAM_DAT_DIR."); - - // Default layout: vitals (0x2100006C), unless a dump slug or markup is requested. - if (!mockup && layoutId is null && markupPath is null && dumpSlug is null) - layoutId = 0x2100006Cu; - - return new StudioOptions( - datDir, - layoutId, - markupPath, - dumpSlug, - dumpFile, - screenshotPath, - mockup, - capabilityReportPath, - audioSmoke); - } - - /// - /// Resolve the dump file path for this session: - /// - /// if explicitly set. - /// Otherwise <solutionRoot>/docs/research/2026-06-25-retail-ui-layout-dump.json. - /// - /// Returns null when neither the override nor the default file exists. - /// - public string? ResolveDumpFile() - { - if (!string.IsNullOrEmpty(DumpFile)) - return DumpFile; - - // Walk up from the App binary to the solution root (same approach as - // ConformanceDats.SolutionRoot in the test project). - var dir = AppContext.BaseDirectory; - while (!string.IsNullOrEmpty(dir)) - { - var candidate = Path.Combine(dir, "docs", "research", - "2026-06-25-retail-ui-layout-dump.json"); - if (File.Exists(candidate)) - return candidate; - dir = Path.GetDirectoryName(dir); - } - return null; - } -} diff --git a/src/AcDream.App/Studio/StudioWindow.cs b/src/AcDream.App/Studio/StudioWindow.cs deleted file mode 100644 index be43d853..00000000 --- a/src/AcDream.App/Studio/StudioWindow.cs +++ /dev/null @@ -1,675 +0,0 @@ -using System.Numerics; -using AcDream.Content; -using AcDream.App.Platform; -using AcDream.App.Audio; -using AcDream.Core.Audio; -using AcDream.App.Rendering; -using AcDream.App.UI; -using AcDream.Runtime.Platform; -using DatReaderWriter; -using Silk.NET.Input; -using Silk.NET.Maths; -using Silk.NET.OpenGL; -using Silk.NET.Windowing; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; -using static Silk.NET.OpenGL.ClearBufferMask; - -namespace AcDream.App.Studio; - -/// -/// Standalone Silk.NET window that boots the production render stack -/// () and previews a single UI panel -/// identified by a . -/// -/// Usage: dotnet run -- ui-studio [dat-dir] [--layout 0xNNNN] [--markup path] [--mockup] -/// -/// Task 3 adds an ImGui IDE on top of the panel FBO: -/// -/// Canvas pane — the panel rendered off-screen via . -/// Tree pane — the element hierarchy; click-to-select. -/// Properties pane — geometry/anchors/flags of the selected element. -/// Click-to-inspect — a left-click in the canvas selects the topmost -/// element under the cursor via . -/// -/// -/// The window is intentionally thin: no game world, no physics, no streaming — -/// just GL + UiHost + the layout under test, identical to how the panel -/// appears inside GameWindow. -/// -public sealed class StudioWindow : IDisposable -{ - private readonly StudioOptions _opts; - private readonly ApplicationPathSet _applicationPaths; - private readonly GraphicalHostPlatformServices _platformServices; - - // Created in OnLoad, released in OnClosing. - private IWindow? _window; - private GL? _gl; - private IInputContext? _input; - private OpenAlAudioEngine? _audio; - private IDatReaderWriter? _dats; - private RenderStack? _stack; - private LayoutSource? _source; - - // Task 3 additions. - private AcDream.UI.ImGui.ImGuiBootstrapper? _imgui; - private PanelFbo? _panelFbo; - private StudioInspector? _inspector; - private UiElement? _panelRoot; // top-level element added to UiRoot (for hit-test + tree) - - // UX-pass additions: panel picker + current-slug tracking. - private string? _currentSlug; // slug of the panel currently displayed (null = non-dump mode) - private string? _dumpFile; // resolved dump file path (once, in OnLoad) - private IReadOnlyList _dumpSlugs = Array.Empty(); // all slugs from the dump - - // Task 4: sample data table — built once in OnLoad and kept alive for the window's lifetime - // so the controller subscriptions (ObjectAdded/ObjectMoved etc.) fire correctly. - private AcDream.Core.Items.ClientObjectTable? _objects; - private IRetainedPanelController? _fixtureController; - - // Headless screenshot mode: set when --screenshot was passed. - // True after the first OnRender fires the screenshot (guard against repeat). - private bool _screenshotDone; - private readonly StudioFrameCloseGate _frameCloseGate = new(); - private GraphicalCapabilityRecord? _capabilities; - private string? _capabilityReportPath; - - public StudioWindow(StudioOptions opts) - : this( - opts, - GraphicalHostPlatformServices.Resolve()) - { - } - - internal StudioWindow( - StudioOptions opts, - ApplicationPathSet applicationPaths) - : this( - opts, - GraphicalHostPlatformServices.Resolve() with - { - Paths = applicationPaths - ?? throw new ArgumentNullException( - nameof(applicationPaths)), - }) - { - } - - internal StudioWindow( - StudioOptions opts, - GraphicalHostPlatformServices platformServices) - { - _opts = opts ?? throw new ArgumentNullException(nameof(opts)); - _platformServices = platformServices - ?? throw new ArgumentNullException(nameof(platformServices)); - _applicationPaths = _platformServices.Paths; - } - - /// - /// Open the window and block until it is closed. - /// Mirrors GameWindow.Run(). - /// - public void Run() - { - _platformServices.ConfigureWindowBackend(); - // Resolve quality settings the same way GameWindow.Run() does - // (SettingsStore → QualitySettings.From → WithEnvOverrides). - var startupStore = new AcDream.UI.Abstractions.Panels.Settings.SettingsStore( - _applicationPaths.SettingsFile); - var startupDisplay = startupStore.LoadDisplay(); - var startupBase = AcDream.UI.Abstractions.Settings.QualitySettings.From(startupDisplay.Quality); - var startupQuality = AcDream.UI.Abstractions.Settings.QualitySettings.WithEnvOverrides(startupBase); - - var options = WindowOptions.Default with - { - Size = new Vector2D(1280, 720), - Title = "acdream UI Studio", - API = new GraphicsAPI( - ContextAPI.OpenGL, - ContextProfile.Core, - ContextFlags.ForwardCompatible, - new APIVersion(4, 3)), - VSync = false, - // MSAA from quality preset — must be baked into the GL context at creation. - Samples = startupQuality.MsaaSamples, - PreferredStencilBufferBits = 8, - // Headless screenshot mode: hide the window so no desktop flash occurs. - // The GL context is still fully valid on a hidden window; FBO rendering - // is off-screen and independent of window visibility. - IsVisible = _opts.ScreenshotPath is null, - }; - - _window = Window.Create(options); - _window.Load += OnLoad; - _window.Update += OnUpdate; - _window.Render += OnRender; - _window.Closing += OnClosing; - _window.Run(); - } - - private void OnLoad() - { - _gl = GL.GetApi(_window!); - _input = _window!.CreateInput(); - _capabilityReportPath = Path.GetFullPath( - _opts.CapabilityReportPath - ?? Path.Combine( - _applicationPaths.DiagnosticsDirectory, - "graphical-capabilities.json")); - _capabilities = GraphicalCapabilityGuard.CaptureVerifyAndWrite( - _gl, - _window!, - _input, - _platformServices, - _capabilityReportPath); - GraphicalCapabilityGuard.ThrowIfUnsupported( - _capabilities, - _capabilityReportPath); - - if (_opts.AudioSmoke) - { - _audio = new OpenAlAudioEngine(); - bool playback = _audio.IsAvailable - && _audio.PlayUiWave( - 0xFFFF_FF01u, - CreateAudioSmokeWave(), - volume: 0.05f); - _capabilities = _capabilities with - { - Audio = new GraphicalAudioCapabilities( - Requested: true, - Available: _audio.IsAvailable, - PlaybackSubmitted: playback, - DisposalComplete: _audio.IsDisposalComplete, - Backend: "OpenAL Soft default device"), - Lifecycle = _capabilities.Lifecycle with - { - OwnedAudioEngineCount = 1, - }, - }; - GraphicalCapabilityReportWriter.Write( - _capabilityReportPath, - _capabilities); - } - - _dats = RuntimeDatCollectionFactory.OpenReadOnly(_opts.DatDir); - - // Build QualitySettings for RenderBootstrap (same as Run() above — re-read - // after the GL context is confirmed, mirroring GameWindow.OnLoad). - var store = new AcDream.UI.Abstractions.Panels.Settings.SettingsStore( - _applicationPaths.SettingsFile); - var display = store.LoadDisplay(); - var quality = AcDream.UI.Abstractions.Settings.QualitySettings.WithEnvOverrides( - AcDream.UI.Abstractions.Settings.QualitySettings.From(display.Quality)); - - _stack = RenderBootstrap.Create( - _gl, - _dats, - new RenderBootstrapOptions( - quality, - _applicationPaths.DiagnosticsDirectory)); - - if (_opts.Mockup) - MockupDesktop.Load(_dats, _stack); - - // Load the panel described by options and add it to the UI tree. - // Task 4b: --dump uses DumpLayout (static retail mockup, no controllers). - // All other modes use LayoutSource + FixtureProvider (production path). - // - // Fix C: pass the per-element font resolver into LayoutSource so that elements - // with a non-zero FontDid get their own dat font at build time. This is wired - // ONLY in the studio path; GameWindow's Import calls continue to pass null so the - // live game path is provably unchanged (follow-up: GameWindow font-resolver wire-up). - _source = new LayoutSource(_dats, _stack.ResolveChrome, _stack.VitalsDatFont, - fontResolve: _stack.ResolveDatFont); - - // Resolve the dump file once (used by OnLoad + by LoadDumpPanel at runtime). - _dumpFile = _opts.ResolveDumpFile(); - if (_dumpFile is not null) - _dumpSlugs = UiDumpModel.ListSlugs(_dumpFile) - .OrderBy(s => s, StringComparer.OrdinalIgnoreCase).ToList(); - - UiElement? root = null; - if (!_opts.Mockup && _opts.DumpSlug is not null) - { - if (_dumpFile is null) - { - Console.Error.WriteLine("[studio] --dump: retail UI dump file not found. " + - "Expected docs/research/2026-06-25-retail-ui-layout-dump.json in the source tree, " + - "or pass --dump-file ."); - root = null; - } - else - { - root = DumpLayout.Load(_dumpFile, _opts.DumpSlug, _stack.ResolveChrome, out var dumpErr); - if (root is null) - Console.Error.WriteLine($"[studio] dump load failed: {dumpErr}"); - else - _currentSlug = _opts.DumpSlug; - } - } - else if (!_opts.Mockup) - { - root = _source.Load(_opts); - if (root is null) - Console.Error.WriteLine($"[studio] panel load failed: {_source.LastError}"); - else - NormalizeSinglePanelRoot(root); - } - - _panelRoot = root; - if (root is not null) - { - _stack.UiHost.Root.AddChild(root); - - // Task 4: populate the panel with sample data via production controllers, - // so inventory / vitals / toolbar panels render with plausible content. - // Dump source is static — no FixtureProvider needed. - if (_opts.DumpSlug is null && _source.CurrentLayout is not null) - { - uint layoutId = _opts.LayoutId ?? 0x2100006Cu; - _objects = SampleData.BuildObjectTable(); - _fixtureController = FixtureProvider.Populate( - layoutId, _source.CurrentLayout, _stack, _objects, _dats); - } - } - - // Task 3: ImGui IDE — interactive mode only. - // Headless screenshot mode needs only PanelFbo; ImGui/inspector/input are skipped. - _panelFbo = new PanelFbo(_gl); - if (_opts.ScreenshotPath is null) - { - // Mockup mode draws UiHost directly to the window, so raw Silk mouse coordinates - // already match UI pixels. Inspector mode draws UiHost inside an ImGui canvas, so - // mouse is forwarded manually from DrawCanvas with canvas-local remapping. - if (_opts.Mockup) - foreach (var mouse in _input.Mice) - _stack.UiHost.WireMouse(mouse); - foreach (var kb in _input.Keyboards) - _stack.UiHost.WireKeyboard(kb); - - if (_opts.Mockup) - return; - - _imgui = new AcDream.UI.ImGui.ImGuiBootstrapper( - _gl, - _window!, - _input); - _inspector = new StudioInspector(); - } - } - - private static WaveData CreateAudioSmokeWave() - { - const int sampleRate = 8_000; - const int sampleCount = 400; - var pcm = new byte[sampleCount * sizeof(short)]; - for (int i = 0; i < sampleCount; i++) - { - double phase = i * (2.0 * Math.PI * 440.0 / sampleRate); - short sample = (short)(Math.Sin(phase) * short.MaxValue * 0.08); - BitConverter.TryWriteBytes( - pcm.AsSpan(i * sizeof(short), sizeof(short)), - sample); - } - - return new WaveData - { - ChannelCount = 1, - SampleRate = sampleRate, - BitsPerSample = 16, - PcmBytes = pcm, - Duration = TimeSpan.FromSeconds( - sampleCount / (double)sampleRate), - }; - } - - private void OnUpdate(double dt) { } - - private void OnRender(double dt) - { - if (_stack is null || _panelFbo is null) return; - RenderStack frameStack = _stack; - frameStack.BeginFrame(); - Exception? renderFailure = null; - bool frameClosed = false; - try - { - - // ── HEADLESS SCREENSHOT PATH ────────────────────────────────────────────── - if (_opts.ScreenshotPath is not null) - { - if (_screenshotDone) return; // fire exactly once - _screenshotDone = true; - - // Pick render size from the loaded root's bounds (clamped to sane limits). - // Fall back to 1280×720 when the root has no explicit size. - int w = 1280, h = 720; - if (!_opts.Mockup && _panelRoot is not null) - { - float rw = _panelRoot.Width; - float rh = _panelRoot.Height; - if (rw >= 1f && rh >= 1f) - { - w = Math.Clamp((int)rw, 256, 2048); - h = Math.Clamp((int)rh, 256, 2048); - } - } - - // Tick once so widget state is initialised (e.g. bar fills). - _stack.UiHost.Tick(dt); - - // Render the panel into the FBO. - _panelFbo.Render(w, h, _stack.UiHost); - - // Read back RGBA pixels (FBO origin = bottom-left). - byte[] pixels = _panelFbo.ReadColorRgba(w, h); - if (pixels.Length == 0) - { - Console.Error.WriteLine("[studio-screenshot] FBO readback returned no pixels."); - _frameCloseGate.Request(); - return; - } - - // Flip rows vertically: FBO bottom-left → PNG top-left. - int stride = w * 4; - byte[] flipped = new byte[pixels.Length]; - for (int row = 0; row < h; row++) - { - System.Buffer.BlockCopy(pixels, row * stride, flipped, (h - 1 - row) * stride, stride); - } - - // Build ImageSharp image and save as PNG. - var path = _opts.ScreenshotPath; - var dir = Path.GetDirectoryName(path); - if (!string.IsNullOrEmpty(dir)) - Directory.CreateDirectory(dir); - - using var img = Image.LoadPixelData(flipped, w, h); - img.SaveAsPng(path); - Console.WriteLine($"[studio-screenshot] wrote {path} ({w}x{h})"); - - _frameCloseGate.Request(); - return; - } - - // ── INTERACTIVE PATH ────────────────────────────────────────────────────── - if (_opts.Mockup) - { - var mockupGl = _stack.Gl; - int mockupW = _window!.Size.X; - int mockupH = _window!.Size.Y; - _stack.UiHost.Tick(dt); - mockupGl.BindFramebuffer(FramebufferTarget.Framebuffer, 0); - mockupGl.Viewport(0, 0, (uint)mockupW, (uint)mockupH); - mockupGl.ClearColor(0.08f, 0.08f, 0.08f, 1f); - mockupGl.Clear(ColorBufferBit | DepthBufferBit); - _stack.UiHost.Draw(new Vector2(mockupW, mockupH)); - return; - } - - if (_imgui is null || _inspector is null) return; - - var gl = _stack.Gl; - int iw = _window!.Size.X; - int ih = _window!.Size.Y; - - // 1. Tick the UI widgets (OnRender's own dt — Update + Render fire with the same delta). - _stack.UiHost.Tick(dt); - - // 2. Render the panel into the off-screen FBO; get the color texture. - // The FBO is the same logical size as the window, so element rects map 1:1 to - // FBO pixels — no scale factor needed when displaying the canvas at full size. - uint panelTex = _panelFbo.Render(iw, ih, _stack.UiHost); - - // 3. Clear the window back-buffer (the dark ImGui background shows behind panes). - gl.ClearColor(0.1f, 0.1f, 0.1f, 1f); - gl.Clear(ColorBufferBit | DepthBufferBit); - - // 4. Begin the ImGui frame. - _imgui.BeginFrame((float)dt); - - // ── Layout constants (fixed pane arrangement, FirstUseEver) ────────────── - // MenuBar: always-on-top main menu bar (~22px) — panel picker lives here so it - // is never covered by the floating panes (replaces the old 40px toolbar). - // Tree: 280px wide on the left, below menu bar. - // Canvas: centre strip between tree and properties. - // Props: 340px wide on the right, below menu bar. - const int kMenuBarH = 22; // ImGui default main menu bar height - const int kTreeW = 280; - const int kPropsW = 340; - int canvasX = kTreeW; - int canvasW = Math.Max(1, iw - kTreeW - kPropsW); - int propsX = iw - kPropsW; - int paneY = kMenuBarH; - int paneH = Math.Max(1, ih - kMenuBarH); - - // 5. Main menu bar — panel picker combo pinned to the window top. - // BeginMainMenuBar returns true when the bar is visible (always is); the combo - // inside it is always-on-top and is never occluded by Tree/Canvas/Props panes. - string? pickedSlug = null; - if (ImGuiNET.ImGui.BeginMainMenuBar()) - { - ImGuiNET.ImGui.SetNextItemWidth(300f); - string preview = _currentSlug ?? "(none)"; - if (ImGuiNET.ImGui.BeginCombo("Panel", preview)) - { - foreach (var slug in _dumpSlugs) - { - bool selected = string.Equals(slug, _currentSlug, - System.StringComparison.OrdinalIgnoreCase); - if (ImGuiNET.ImGui.Selectable(slug, selected) && !selected) - pickedSlug = slug; - if (selected) - ImGuiNET.ImGui.SetItemDefaultFocus(); - } - ImGuiNET.ImGui.EndCombo(); - } - ImGuiNET.ImGui.EndMainMenuBar(); - } - if (pickedSlug is not null) - LoadDumpPanel(pickedSlug); - - // 6. Canvas pane — show the FBO texture; gather canvas mouse events. - var canvasEvt = default(CanvasInputEvent); - if (panelTex != 0) - canvasEvt = _inspector.DrawCanvas( - (nint)panelTex, iw, ih, - canvasX, canvasW, paneY, paneH); - - // 7. Forward canvas mouse events to the panel UiHost or the inspector tree. - // - // Coordinate mapping (see StudioInspector.DrawCanvas summary): - // panel-local pixel = raw_mouse - ImGui.GetItemRectMin() (1:1 scale, no Y flip) - // The image is drawn V-flipped (uv0.Y=1, uv1.Y=0) so screen top = panel Y=0. - // - // Interact mode (default): canvas mouse events go directly to UiRoot so elements respond. - // OnMouseMove + OnMouseDown/Up + OnScroll are all forwarded. - // A Console.WriteLine confirms each forwarded left-click for live verification. - // - // Inspect mode: left-click hit-tests and selects the element in the tree (old behavior). - // OnMouseMove is still forwarded so hover/tooltip state in the panel stays live. - if (canvasEvt.IsHovered) - { - int mx = canvasEvt.MouseX; - int my = canvasEvt.MouseY; - var root = _stack.UiHost.Root; - - // Always forward mouse-move so hover highlights / tooltips in the panel work. - root.OnMouseMove(mx, my); - - if (_inspector.InteractMode) - { - // ── Interact: live panel interaction ────────────────────────────── - if (canvasEvt.LeftDown) - { - Console.WriteLine($"[studio] canvas click → panel ({mx}, {my})"); - root.OnMouseDown(UiMouseButton.Left, mx, my); - } - if (canvasEvt.LeftUp) - root.OnMouseUp(UiMouseButton.Left, mx, my); - if (canvasEvt.ScrollDelta != 0) - root.OnScroll(canvasEvt.ScrollDelta); - } - else - { - // ── Inspect: click selects an element in the tree ───────────────── - if (canvasEvt.LeftDown) - { - var hit = root.Pick(mx, my); - if (hit is not null) - _inspector.Selected = hit; - } - } - } - - // 8. Element tree pane. - if (_panelRoot is not null) - _inspector.DrawTree(_panelRoot, 0, paneY, kTreeW, paneH); - - // 9. Properties pane. - _inspector.DrawProperties(propsX, paneY, kPropsW, paneH); - - // 9. Finalise ImGui and flush draw data to the window. - _imgui.Render(); - } - catch (Exception ex) - { - renderFailure = ex; - throw; - } - finally - { - try - { - frameStack.EndFrame(); - frameClosed = true; - } - catch (Exception closeFailure) when (renderFailure is not null) - { - throw new AggregateException( - "UI Studio rendering failed and its GPU frame could not be closed.", - renderFailure, - closeFailure); - } - - if (frameClosed) - _frameCloseGate.CompleteFrame(() => _window?.Close()); - } - } - - /// - /// Load a different dump panel at runtime (no relaunch required). - /// Removes the current from the UI tree, - /// loads the named slug from the dump, and installs the new root. - /// Resets to null. - /// No-op when the dump file is not available or the slug fails to load. - /// - public void LoadDumpPanel(string slug) - { - if (_stack is null || _inspector is null) return; - if (_dumpFile is null) - { - Console.Error.WriteLine("[studio] LoadDumpPanel: dump file not available."); - return; - } - - // Remove the existing panel root from the tree. Fixture controllers - // own subscriptions into the sample table and end with their panel. - _fixtureController?.Dispose(); - _fixtureController = null; - if (_panelRoot is not null) - { - _stack.UiHost.Root.RemoveChild(_panelRoot); - _panelRoot = null; - } - - // Load the new panel. - var newRoot = DumpLayout.Load(_dumpFile, slug, _stack.ResolveChrome, out var err); - if (newRoot is null) - { - Console.Error.WriteLine($"[studio] LoadDumpPanel('{slug}') failed: {err}"); - _currentSlug = null; - return; - } - - _stack.UiHost.Root.AddChild(newRoot); - _panelRoot = newRoot; - _currentSlug = slug; - _inspector.Selected = null; - } - - /// - /// Studio dat-layout previews show one panel in isolation, not at its live-game - /// screen position. Some top-level layouts (inventory: x=500,y=138) otherwise - /// draw entirely outside the root-sized screenshot FBO. - /// - internal static void NormalizeSinglePanelRoot(UiElement root) - { - root.Left = 0f; - root.Top = 0f; - root.Anchors = AnchorEdges.None; - } - - private void OnClosing() - { - ReleaseContextResources(); - } - - public void Dispose() - { - ReleaseContextResources(); - _window?.Dispose(); - _window = null; - UpdateFinalCapabilityReport(); - } - - private void ReleaseContextResources() - { - _fixtureController?.Dispose(); - _fixtureController = null; - _imgui?.Dispose(); - _panelFbo?.Dispose(); - _imgui = null; - _panelFbo = null; - // If OnClosing wasn't called (e.g. an exception before Run() completed), dispose the FULL - // stack anyway — the review flagged that disposing only UiHost here leaked the rest. - _stack?.Dispose(); - _dats?.Dispose(); - _audio?.Dispose(); - _input?.Dispose(); - _gl?.Dispose(); - _dats = null; - _stack = null; - _audio = null; - _input = null; - _gl = null; - } - - private void UpdateFinalCapabilityReport() - { - if (_capabilities is null - || string.IsNullOrWhiteSpace(_capabilityReportPath)) - { - return; - } - - _capabilities = _capabilities with - { - Audio = _capabilities.Audio with - { - DisposalComplete = true, - }, - Lifecycle = new GraphicalSmokeLifecycleCapabilities( - OwnedWindowCount: 0, - OwnedGlApiCount: 0, - OwnedInputContextCount: 0, - OwnedAudioEngineCount: 0, - ShutdownComplete: true), - }; - GraphicalCapabilityReportWriter.Write( - _capabilityReportPath, - _capabilities); - } -} diff --git a/src/AcDream.App/Studio/UiDumpModel.cs b/src/AcDream.App/Studio/UiDumpModel.cs deleted file mode 100644 index c33e4b9e..00000000 --- a/src/AcDream.App/Studio/UiDumpModel.cs +++ /dev/null @@ -1,198 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace AcDream.App.Studio; - -// ───────────────────────────────────────────────────────────────────────────── -// UiDumpModel — POCOs for docs/research/2026-06-25-retail-ui-layout-dump.json -// -// Schema (v1): -// { "version":1, "panels":[ { "id":int, "slug":string, "title":string, -// "bucket":string, "parent_slug":string|null, -// "width":int, "height":int, -// "nodes":[ { "traversal_index":int, "element_id":int, -// "layout_id":int, "parent_layout_id":int|null, -// "parent_traversal_index":int|null, "base_layout_id":int, -// "rect":{x,y,width,height}, -// "widget_kind":"Group"|"Sprite"|"Button"|"Scrollbar"|"Slider", -// "state_set":{ "default_image":{image_id,alpha_image_id}|null, -// "states":[{state_id,image:{...}}] } -// } ] } ] } -// -// All ids in the dump are DECIMAL ints (e.g. element_id=268435925 = 0x100001D5, -// image_id=100693194 = 0x060074CA). Cast to uint before use in dat/GL APIs. -// -// Rect coordinates are ABSOLUTE (screen-space origin = panel's design position -// in retail layout, NOT relative to the parent). DumpLayout.Load converts them -// to parent-relative when building the UiElement tree. -// ───────────────────────────────────────────────────────────────────────────── - -/// Top-level container for the retail UI layout dump. -public sealed class UiDump -{ - [JsonPropertyName("version")] - public int Version { get; set; } - - [JsonPropertyName("panels")] - public List Panels { get; set; } = new(); -} - -/// One panel (window) exported from the retail UI. -public sealed class DumpPanel -{ - [JsonPropertyName("id")] - public long Id { get; set; } - - [JsonPropertyName("slug")] - public string Slug { get; set; } = ""; - - [JsonPropertyName("title")] - public string Title { get; set; } = ""; - - [JsonPropertyName("bucket")] - public string Bucket { get; set; } = ""; - - [JsonPropertyName("parent_slug")] - public string? ParentSlug { get; set; } - - [JsonPropertyName("width")] - public float Width { get; set; } - - [JsonPropertyName("height")] - public float Height { get; set; } - - [JsonPropertyName("nodes")] - public List Nodes { get; set; } = new(); -} - -/// One element node within a panel's traversal list. -public sealed class DumpNode -{ - [JsonPropertyName("traversal_index")] - public int TraversalIndex { get; set; } - - [JsonPropertyName("element_id")] - public long ElementId { get; set; } - - [JsonPropertyName("layout_id")] - public long LayoutId { get; set; } - - [JsonPropertyName("parent_layout_id")] - public long? ParentLayoutId { get; set; } - - [JsonPropertyName("parent_traversal_index")] - public int? ParentTraversalIndex { get; set; } - - [JsonPropertyName("base_layout_id")] - public long BaseLayoutId { get; set; } - - [JsonPropertyName("rect")] - public DumpRect Rect { get; set; } = new(); - - [JsonPropertyName("widget_kind")] - public string WidgetKind { get; set; } = "Group"; - - [JsonPropertyName("state_set")] - public DumpStateSet StateSet { get; set; } = new(); -} - -/// Absolute screen-space rect (see comment above — must subtract parent rect for UiElement). -public sealed class DumpRect -{ - [JsonPropertyName("x")] - public float X { get; set; } - - [JsonPropertyName("y")] - public float Y { get; set; } - - [JsonPropertyName("width")] - public float Width { get; set; } - - [JsonPropertyName("height")] - public float Height { get; set; } -} - -/// State set for a node — default image plus per-state overrides. -public sealed class DumpStateSet -{ - [JsonPropertyName("default_image")] - public DumpImage? DefaultImage { get; set; } - - [JsonPropertyName("states")] - public List States { get; set; } = new(); -} - -/// Image reference (RenderSurface dat id + optional separate alpha surface). -public sealed class DumpImage -{ - [JsonPropertyName("image_id")] - public long ImageId { get; set; } - - [JsonPropertyName("alpha_image_id")] - public long? AlphaImageId { get; set; } -} - -/// A named state override. -public sealed class DumpState -{ - [JsonPropertyName("state_id")] - public int StateId { get; set; } - - [JsonPropertyName("image")] - public DumpImage Image { get; set; } = new(); -} - -// ───────────────────────────────────────────────────────────────────────────── -// Helper statics -// ───────────────────────────────────────────────────────────────────────────── - -/// -/// Parsing helpers for the retail UI dump JSON. -/// -public static class UiDumpModel -{ - private static readonly JsonSerializerOptions _opts = new() - { - PropertyNameCaseInsensitive = true, - AllowTrailingCommas = true, - ReadCommentHandling = JsonCommentHandling.Skip, - }; - - /// Parse the full dump from a file path. Returns null on failure. - public static UiDump? Parse(string path) - { - try - { - using var stream = File.OpenRead(path); - return JsonSerializer.Deserialize(stream, _opts); - } - catch - { - return null; - } - } - - /// - /// Return the list of slugs in the dump (for smoke-testing every panel). - /// Returns an empty list if the file cannot be parsed. - /// - public static IReadOnlyList ListSlugs(string path) - { - var dump = Parse(path); - if (dump is null) return Array.Empty(); - return dump.Panels.Select(p => p.Slug).ToList(); - } - - /// - /// Pick the sprite id to use for a node: prefer default_image.image_id; - /// fall back to states[0].image.image_id; return 0 if neither exists. - /// - public static uint PickImageId(DumpNode node) - { - if (node.StateSet.DefaultImage is { ImageId: > 0 } di) - return (uint)di.ImageId; - if (node.StateSet.States.Count > 0 && node.StateSet.States[0].Image.ImageId > 0) - return (uint)node.StateSet.States[0].Image.ImageId; - return 0u; - } -} diff --git a/src/AcDream.App/UI/Layout/SampleData.cs b/src/AcDream.App/UI/Layout/SampleData.cs new file mode 100644 index 00000000..7ac43c9b --- /dev/null +++ b/src/AcDream.App/UI/Layout/SampleData.cs @@ -0,0 +1,94 @@ +namespace AcDream.App.UI.Layout; + +/// +/// Static sample character-sheet data. It is the character-sheet fallback +/// used when no live session has produced a real +/// yet, and the fixture the character/attribute UI panel tests bind against. +/// +public static class SampleData +{ + /// + /// Returns a representative . Values are + /// plausible retail-scale numbers so the report renders with + /// well-proportioned text in all sections. + /// + public static CharacterSheet SampleCharacter() => SampleCharacter(null); + + public static CharacterSheet SampleCharacter(string? name) => new() + { + Name = string.IsNullOrWhiteSpace(name) ? "Studio Player" : name, + Level = 126, + Gender = "Female", + Heritage = "Aluvian", + Title = "the Adventurer", + BirthDate = "January 5, 2001", + PlayTime = "2 years, 114 days, 4 hours", + Deaths = 42, + + PkStatus = "Non-Player Killer", + TotalXp = 1_250_000_000, + XpToNextLevel = 42_000_000, + XpFraction = 0.63f, + + // Vitals: retail screenshot spec (Pass 1 acceptance criteria §Goal). + HealthCurrent = 5, HealthMax = 5, + StaminaCurrent = 10, StaminaMax = 10, + ManaCurrent = 10, ManaMax = 10, + + // Attributes: Strength + Quickness = 200; all others = 10 (retail screenshot spec §Goal). + Strength = 200, + Endurance = 10, + Quickness = 200, + Coordination = 10, + Focus = 10, + Self = 10, + + UnspentSkillCredits = 12, + SpecializedSkillCredits = 4, + ChessRank = 12, + FishingSkill = 4, + + // Available skill credits (retail InqInt(0x18)); shown in Attributes tab footer State-A. + SkillCredits = 96, + + // Unassigned (banked) XP (retail InqInt64(2)); footer State-A line-2 value. + UnassignedXp = 87_757_321_741L, + + // Raise costs in retail display order (Strength, Endurance, Coordination, Quickness, + // Focus, Self, Health, Stamina, Mana). + // Str@200 = maxed → 0 (disabled). Quickness@200 = maxed → 0. Others @10 → affordable. + // Focus@10 → 110 matches the authoritative retail screenshot (spec §4). + // Formula bracket at value=10: ExperienceToAttributeLevel(11) − ExperienceToAttributeLevel(10). + AttributeRaiseCosts = new long[] { 0L, 95L, 100L, 0L, 110L, 105L, 90L, 88L, 112L }, + AttributeRaise10Costs = new long[] { 0L, 950L, 1_000L, 0L, 1_100L, 1_050L, 900L, 880L, 1_120L }, + + // Real SkillTable icon IDs and train/specialize costs from client_portal.dat + // SkillTable 0x0E000004. gmSkillUI groups/sorts these at bind time. + Skills = new CharacterSkill[] + { + new( 6, "Melee Defense", 0x06000165u, CharacterSkillAdvancementClass.Specialized, 350, 354, false, 10, 20, 18_250_000L, 182_500_000L), + new(34, "War Magic", 0x06001365u, CharacterSkillAdvancementClass.Specialized, 280, 285, false, 16, 28, 11_100_000L, 111_000_000L), + + new(14, "Arcane Lore", 0x0600016Eu, CharacterSkillAdvancementClass.Trained, 260, 269, false, 4, 6, 7_500_000L, 75_000_000L), + new(33, "Life Magic", 0x06001364u, CharacterSkillAdvancementClass.Trained, 250, 252, false, 12, 20, 6_800_000L, 68_000_000L), + new(47, "Missile Weapons", 0x0600015Fu, CharacterSkillAdvancementClass.Trained, 220, 221, false, 6, 12, 5_250_000L, 52_500_000L), + + new(21, "Healing", 0x06000133u, CharacterSkillAdvancementClass.Untrained, 10, 10, true, 6, 10, 0L), + new(22, "Jump", 0x0600016Bu, CharacterSkillAdvancementClass.Untrained, 210, 210, true, 0, 4, 0L), + new(36, "Loyalty", 0x06001367u, CharacterSkillAdvancementClass.Untrained, 10, 10, true, 0, 2, 0L), + new(24, "Run", 0x06000173u, CharacterSkillAdvancementClass.Untrained, 390, 390, true, 0, 4, 0L), + + new(38, "Alchemy", 0x060019E4u, CharacterSkillAdvancementClass.Untrained, 10, 10, false, 6, 12, 0L), + new(39, "Cooking", 0x06001A54u, CharacterSkillAdvancementClass.Untrained, 10, 10, false, 4, 8, 0L), + new(37, "Fletching", 0x06001A55u, CharacterSkillAdvancementClass.Untrained, 10, 10, false, 4, 8, 0L), + }, + + CharacterInfoProperties = new Dictionary + { + [0x162u] = 2, // Swords melee mastery + }, + + BurdenCurrent = 1200, + BurdenMax = 4500, + }; +} diff --git a/src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj b/src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj deleted file mode 100644 index 66f5bbad..00000000 --- a/src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - net10.0 - enable - enable - latest - true - - - - - - - - - - - - - diff --git a/src/AcDream.UI.ImGui/ImGuiBootstrapper.cs b/src/AcDream.UI.ImGui/ImGuiBootstrapper.cs deleted file mode 100644 index 23d46536..00000000 --- a/src/AcDream.UI.ImGui/ImGuiBootstrapper.cs +++ /dev/null @@ -1,112 +0,0 @@ -using Silk.NET.Input; -using Silk.NET.OpenGL; -using Silk.NET.OpenGL.Extensions.ImGui; -using Silk.NET.Windowing; - -namespace AcDream.UI.ImGui; - -/// -/// Owns the ImGuiController from Silk.NET.OpenGL.Extensions.ImGui, -/// which handles the whole Silk.NET ↔ ImGui.NET integration: -/// -/// Creates the ImGui context + OpenGL3 backend using Silk.NET's GL binding -/// (no GLFW / SDL dependency — unlike Hexa.NET.ImGui, which assumed one). -/// Subscribes to Silk.NET's window + input events to drive IO. -/// Per frame: Update(dt) calls ImGui.NewFrame(); Render() -/// calls ImGui.Render() + uploads draw data via its OpenGL3 backend. -/// -/// -/// -/// Instance-scoped rather than static so GL-context lifetime is explicit. -/// GameWindow owns the one instance and disposes on shutdown. -/// -/// -/// -/// History: tried Hexa.NET.ImGui + Hexa.NET.ImGui.Backends.OpenGL3 first -/// per the original plan, but its native OpenGL3 backend resolves GL functions -/// via GLFW / SDL internally and crashed (0xC0000005) in InitNative without -/// one of those present. Pivoted to the official Silk.NET extension on 2026-04-25. -/// -/// -public interface IImGuiBootstrapper : IDisposable -{ - void BeginFrame(float deltaSeconds); - void Render(); - void AbortFrame(); -} - -/// -/// The upstream Silk controller does not expose a partially constructed -/// instance. A constructor failure therefore requires teardown of the owning -/// GL/window host instead of being treated as a recoverable optional-frontend -/// failure. -/// -public sealed class ImGuiBootstrapperConstructionException : Exception -{ - internal ImGuiBootstrapperConstructionException(Exception innerException) - : base( - "Silk ImGui construction did not publish a cleanup owner; " + - "the graphics host must be torn down.", - innerException) - { - } -} - -public sealed class ImGuiBootstrapper : IImGuiBootstrapper -{ - private readonly ImGuiController _controller; - - public ImGuiBootstrapper(GL gl, IView window, IInputContext input) - { - ArgumentNullException.ThrowIfNull(gl); - ArgumentNullException.ThrowIfNull(window); - ArgumentNullException.ThrowIfNull(input); - // ImGuiController constructor handles: - // - ImGui.CreateContext() - // - ImGuiOpenGL3 shader + vertex-buffer init (via Silk.NET GL) - // - Keyboard + mouse event subscription (bound to Silk.NET IInputContext) - // - Default style = dark - nint priorContext = ImGuiNET.ImGui.GetCurrentContext(); - try - { - _controller = new ImGuiController(gl, window, input); - } - catch (Exception failure) - { - nint partialContext = ImGuiNET.ImGui.GetCurrentContext(); - if (partialContext != 0 && partialContext != priorContext) - { - ImGuiNET.ImGui.DestroyContext(partialContext); - ImGuiNET.ImGui.SetCurrentContext(priorContext); - } - throw new ImGuiBootstrapperConstructionException(failure); - } - } - - /// - /// Begin an ImGui frame. Call BEFORE any ImGui.* widget calls. - /// Internally: consumes buffered input events, calls ImGui.NewFrame(). - /// - public void BeginFrame(float deltaSeconds) => _controller.Update(deltaSeconds); - - /// - /// Finalise the ImGui frame and draw to the framebuffer. Call AFTER all - /// panel draws, within the same frame as . The - /// OpenGL3 backend save/restores the GL state it touches (shader, VAO, - /// texture, blend, scissor); state not in its save-list (e.g. - /// GL_FRAMEBUFFER_SRGB) is caller's responsibility. - /// - public void Render() => _controller.Render(); - - /// - /// Close an unfinished frame through the owning Silk controller. Silk's - /// controller clears its private frame-begun flag before uploading draw - /// data, so this is both the correct abort path for pre-render failures and - /// an idempotent no-op when a render upload failed after logical close. - /// Calling raw ImGui.EndFrame here would leave the controller's state - /// open and poison its next call. - /// - public void AbortFrame() => _controller.Render(); - - public void Dispose() => _controller.Dispose(); -} diff --git a/src/AcDream.UI.ImGui/ImGuiPanelHost.cs b/src/AcDream.UI.ImGui/ImGuiPanelHost.cs deleted file mode 100644 index d9a3a4c1..00000000 --- a/src/AcDream.UI.ImGui/ImGuiPanelHost.cs +++ /dev/null @@ -1,46 +0,0 @@ -using AcDream.UI.Abstractions; - -namespace AcDream.UI.ImGui; - -/// -/// implementation for the ImGui backend. Owns the -/// registered panel set; iterates + draws every frame when the caller is -/// inside an ImGui frame (between ImGui.NewFrame and -/// ImGui.Render). -/// -/// -/// This class does not call ImGui.NewFrame / ImGui.Render -/// itself. Those belong to the caller (GameWindow) so GL-state -/// ownership is explicit and the render-loop integration point is obvious. -/// -/// -public sealed class ImGuiPanelHost : IPanelHost -{ - private readonly Dictionary _panels = new(); - private readonly ImGuiPanelRenderer _renderer = new(); - - /// - public void Register(IPanel panel) - { - ArgumentNullException.ThrowIfNull(panel); - _panels[panel.Id] = panel; // idempotent by Id - } - - /// - public void Unregister(string panelId) => _panels.Remove(panelId); - - /// - public void RenderAll(PanelContext ctx) - { - // Order-independent — ImGui windows stack in the order they're drawn - // for focus purposes but we have <=1 panel in D.2a. - foreach (var panel in _panels.Values) - { - if (!panel.IsVisible) continue; - panel.Render(ctx, _renderer); - } - } - - /// Current registered count (for diagnostics). - public int Count => _panels.Count; -} diff --git a/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs b/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs deleted file mode 100644 index 4aa94ae6..00000000 --- a/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs +++ /dev/null @@ -1,256 +0,0 @@ -using System.Numerics; -using AcDream.UI.Abstractions; -using ImGuiNET; - -namespace AcDream.UI.ImGui; - -/// -/// implemented as thin wrappers around -/// ImGui.NET calls. This is the ONLY place where ImGuiNET types appear -/// outside of bootstrap plumbing — panels that need a feature must -/// extend the abstraction here, not by importing ImGuiNET in panel -/// files. -/// -public sealed class ImGuiPanelRenderer : IPanelRenderer -{ - /// - public bool Begin(string title) => ImGuiNET.ImGui.Begin(title); - - /// - public void End() => ImGuiNET.ImGui.End(); - - /// - public void Text(string text) => ImGuiNET.ImGui.TextUnformatted(text); - - /// - public void SameLine() => ImGuiNET.ImGui.SameLine(); - - /// - public void Separator() => ImGuiNET.ImGui.Separator(); - - /// - public void ProgressBar(float fraction, float width, string? overlay = null) - { - // Clamp defensively; ImGui clamps internally but the abstraction - // contract promises to handle out-of-range values. - if (fraction < 0f) fraction = 0f; - else if (fraction > 1f) fraction = 1f; - - var size = new Vector2(width, 0f); // height 0 → ImGui picks based on font - ImGuiNET.ImGui.ProgressBar(fraction, size, overlay ?? string.Empty); - } - - // -- Phase I.1 widget extensions --------------------------------- - - /// - public void TextColored(Vector4 rgba, string text) - => ImGuiNET.ImGui.TextColored(rgba, text); - - /// - public bool CollapsingHeader(string label, bool defaultOpen = true) - => ImGuiNET.ImGui.CollapsingHeader( - label, - defaultOpen ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None); - - /// - public bool TreeNode(string label) => ImGuiNET.ImGui.TreeNode(label); - - /// - public void TreePop() => ImGuiNET.ImGui.TreePop(); - - /// - public bool Checkbox(string label, ref bool value) - => ImGuiNET.ImGui.Checkbox(label, ref value); - - /// - public bool Button(string label) => ImGuiNET.ImGui.Button(label); - - /// - public bool Combo(string label, ref int selectedIndex, string[] items) - => ImGuiNET.ImGui.Combo(label, ref selectedIndex, items, items.Length); - - /// - public bool SliderFloat(string label, ref float value, float min, float max) - => ImGuiNET.ImGui.SliderFloat(label, ref value, min, max); - - /// - public void PlotLines( - string label, - float[] values, - int count, - int offset = 0, - string? overlay = null, - float? min = null, - float? max = null, - Vector2? size = null) - { - // ImGui.NET 1.91.6.1's PlotLines binding takes `ref float values` - // (pointer-to-first-element semantics) plus a separate values_count - // and values_offset. The "no fixed bound" / "default size" sentinels - // are float.MaxValue and Vector2.Zero respectively — we pass those - // when the caller leaves the optional args null. - if (count <= 0 || values.Length == 0) - { - // Nothing to plot — emit the label so layout doesn't shift but - // skip the native call (ref to values[0] would NRE on empty). - ImGuiNET.ImGui.TextUnformatted(label); - return; - } - - float scaleMin = min ?? float.MaxValue; - float scaleMax = max ?? float.MaxValue; - Vector2 graphSize = size ?? Vector2.Zero; - ImGuiNET.ImGui.PlotLines( - label, - ref values[0], - count, - offset, - overlay ?? string.Empty, - scaleMin, - scaleMax, - graphSize); - } - - /// - public void BeginTable(string id, int columns) - => ImGuiNET.ImGui.BeginTable(id, columns); - - /// - public void TableNextColumn() => ImGuiNET.ImGui.TableNextColumn(); - - /// - public void EndTable() => ImGuiNET.ImGui.EndTable(); - - /// - public bool InputTextSubmit(string label, ref string buffer, int maxLen, out string? submitted) - { - // EnterReturnsTrue: the call returns true on the frame the user - // pressed Enter. On every other frame ImGui still mutates `buffer` - // as the user types; we just don't surface a submit. - bool entered = ImGuiNET.ImGui.InputText( - label, - ref buffer, - (uint)maxLen, - ImGuiInputTextFlags.EnterReturnsTrue); - if (entered) - { - submitted = buffer; - buffer = string.Empty; // contract: clear for next frame - return true; - } - submitted = null; - return false; - } - - /// - public void Spacing() => ImGuiNET.ImGui.Spacing(); - - /// - public void Dummy(Vector2 size) => ImGuiNET.ImGui.Dummy(size); - - /// - public void TextWrapped(string text) => ImGuiNET.ImGui.TextWrapped(text); - - // -- Phase J Tier 3 — scrollable child for chat-style layouts --------- - - /// - public bool BeginChild(string id, Vector2 size, bool border = false) - { - // ImGuiChildFlags has changed names across ImGui.NET versions - // (Border vs Borders); 0x01 is the stable bit value for "draw - // a border". Casting from a numeric literal sidesteps the - // version-skew without requiring a hard reference to either - // enum spelling. - bool open = ImGuiNET.ImGui.BeginChild(id, size, (ImGuiChildFlags)(border ? 0x01 : 0)); - if (open) - { - // Title-bar-only drag fix (chat tail specifically): empty - // clicks inside a scrollable child fall through to the - // parent window for drag-init, which is exactly what the - // user reported in the chat panel ("clicking anywhere - // moves the window"). An InvisibleButton sized to the - // child's content region absorbs those clicks so they - // don't propagate. Real widgets drawn afterwards still - // claim their own clicks (click priority = "last drawn, - // first checked"). Wheel scrolling is window-level, not - // item-level, so the absorber doesn't interfere with - // the chat tail's auto-scroll. - // - // Scoped to BeginChild only (NOT Begin) because Begin's - // body might host tab bars whose hit-testing competes with - // an absorber on equal terms — adding it at Begin level - // broke Settings tab clicks. - var avail = ImGuiNET.ImGui.GetContentRegionAvail(); - if (avail.X > 0f && avail.Y > 0f) - { - var savedCursor = ImGuiNET.ImGui.GetCursorPos(); - ImGuiNET.ImGui.InvisibleButton("##childbodyabsorb", avail); - ImGuiNET.ImGui.SetCursorPos(savedCursor); - } - } - return open; - } - - /// - public void EndChild() => ImGuiNET.ImGui.EndChild(); - - /// - public float FrameHeightWithSpacing() => ImGuiNET.ImGui.GetFrameHeightWithSpacing(); - - /// - public void SetScrollHereY(float ratio) => ImGuiNET.ImGui.SetScrollHereY(ratio); - - /// - public void SetKeyboardFocusHere() => ImGuiNET.ImGui.SetKeyboardFocusHere(); - - // -- Phase K.3 — main menu bar ----------------------------------------- - - /// - 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) - => shortcut is null - ? ImGuiNET.ImGui.MenuItem(label) - : ImGuiNET.ImGui.MenuItem(label, shortcut); - - // -- Tab bar ----------------------------------------------------------- - - /// - public bool BeginTabBar(string id) => ImGuiNET.ImGui.BeginTabBar(id); - - /// - public void EndTabBar() => ImGuiNET.ImGui.EndTabBar(); - - /// - public bool BeginTabItem(string label) => ImGuiNET.ImGui.BeginTabItem(label); - - /// - public void EndTabItem() => ImGuiNET.ImGui.EndTabItem(); - - // -- Selectable / copyable text --------------------------------------- - - /// - public void TextMultilineReadOnly(string id, string content, Vector2 size) - { - // ImGui's InputTextMultiline takes a `ref string` even with the - // ReadOnly flag — we just hand it a local copy. maxLength caps - // what the user could type if ReadOnly were ever cleared; we - // size it to the current content (+1 for ImGui's internal NUL - // terminator in some bindings). Min of 1 keeps the empty case - // from confusing native bindings. - string buffer = content; - uint maxLen = (uint)System.Math.Max(content.Length + 1, 1); - ImGuiNET.ImGui.InputTextMultiline(id, ref buffer, maxLen, size, - ImGuiInputTextFlags.ReadOnly); - } -} diff --git a/tests/AcDream.App.Tests/Composition/SettingsDevToolsCompositionTests.cs b/tests/AcDream.App.Tests/Composition/SettingsDevToolsCompositionTests.cs deleted file mode 100644 index 9dd79291..00000000 --- a/tests/AcDream.App.Tests/Composition/SettingsDevToolsCompositionTests.cs +++ /dev/null @@ -1,526 +0,0 @@ -using System.Numerics; -using System.Reflection; -using System.Runtime.CompilerServices; -using AcDream.App.Combat; -using AcDream.App.Composition; -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.Runtime.Gameplay; -using AcDream.Core.Player; -using AcDream.Core.Spells; -using AcDream.UI.Abstractions.Input; -using AcDream.UI.Abstractions.Panels.Settings; -using AcDream.UI.Abstractions.Settings; -using AcDream.UI.ImGui; -using Silk.NET.Input; -using Silk.NET.Maths; -using Silk.NET.OpenGL; -using Silk.NET.Windowing; - -namespace AcDream.App.Tests.Composition; - -public sealed class SettingsDevToolsCompositionTests -{ - [Fact] - public void DisabledFrontendAppliesSettingsAndAcquiresNothingOptional() - { - using var fixture = new Fixture(enabled: false); - - SettingsDevToolsResult result = fixture.Compose(); - - Assert.Null(result.DevTools); - Assert.False( - fixture.Dependencies.Runtime.CaptureOwnership().IsDisposeRequested); - Assert.Equal(1, fixture.Startup.DisplayCalls); - Assert.Equal(1, fixture.Startup.AudioCalls); - Assert.Equal(0, fixture.Factory.Calls); - Assert.Equal( - [SettingsDevToolsCompositionPoint.SettingsApplied], - fixture.Points); - } - - [Fact] - public void UnsupportedInputDisablesBeforeAcquiringFrontendResources() - { - using var fixture = new Fixture(inputSupported: false); - - SettingsDevToolsResult result = fixture.Compose(); - - Assert.Null(result.DevTools); - Assert.Null(fixture.Factory.Input); - Assert.Null(fixture.Factory.Bootstrap); - Assert.Equal(1, fixture.Factory.Calls); - } - - [Fact] - public void EnabledFrontendPublishesOneCompleteOwnerAfterEveryEdge() - { - using var fixture = new Fixture(); - - SettingsDevToolsResult result = fixture.Compose(); - - Assert.Same(fixture.Publication.Owner, result.DevTools); - Assert.Equal( - Enum.GetValues(), - fixture.Points); - Assert.True(fixture.Factory.Input!.Activated); - Assert.Equal(4, fixture.Factory.Backend!.LayoutCalls); - Assert.Equal(1, fixture.Publication.PublishCalls); - } - - [Theory] - [MemberData(nameof(OptionalFailurePoints))] - public void OptionalPrefixFailureDisablesOnlyAfterCompleteReverseCleanup( - int pointValue) - { - var point = (SettingsDevToolsCompositionPoint)pointValue; - using var fixture = new Fixture(failurePoint: point); - - SettingsDevToolsResult result = fixture.Compose(); - - Assert.Null(result.DevTools); - Assert.Null(fixture.Publication.Owner); - Assert.Equal( - Enum.GetValues() - .TakeWhile(candidate => candidate <= point), - fixture.Points); - if (fixture.Factory.Input is { } input) - Assert.True(input.IsDisposalComplete); - if (fixture.Factory.Bootstrap is { } bootstrap) - Assert.Equal(1, bootstrap.DisposeCalls); - if (fixture.Factory.Backend is { } backend) - Assert.Equal(1, backend.DisposeCalls); - } - - public static TheoryData OptionalFailurePoints() - { - var data = new TheoryData(); - foreach (SettingsDevToolsCompositionPoint point in - Enum.GetValues()) - { - if (point is SettingsDevToolsCompositionPoint.SettingsApplied - or SettingsDevToolsCompositionPoint.DevToolsPublished) - { - continue; - } - data.Add((int)point); - } - return data; - } - - [Fact] - public void FailureAfterPublicationPropagatesToLifetimeOwner() - { - using var fixture = new Fixture( - failurePoint: SettingsDevToolsCompositionPoint.DevToolsPublished); - - Assert.Throws(fixture.Compose); - - Assert.NotNull(fixture.Publication.Owner); - Assert.False(fixture.Publication.Owner!.IsDisposalComplete); - } - - [Fact] - public void CleanupFailurePropagatesAndRetrySkipsCompletedOperations() - { - using var fixture = new Fixture( - failurePoint: SettingsDevToolsCompositionPoint.InitialLayoutApplied, - backendDisposeFailures: 1); - - var failure = Assert.Throws(fixture.Compose); - Assert.False(failure.IsCleanupComplete); - Assert.Equal(1, fixture.Factory.Backend!.DisposeCalls); - Assert.True(fixture.Factory.Input!.IsDisposalComplete); - - failure.RetryCleanup(); - - Assert.True(failure.IsCleanupComplete); - Assert.Equal(2, fixture.Factory.Backend.DisposeCalls); - Assert.Equal(1, fixture.Factory.Input.DisposeCalls); - } - - [Fact] - public void BootstrapConstructionWithoutCleanupOwnerAbortsHostStartup() - { - var bootstrapFailure = (ImGuiBootstrapperConstructionException) - Activator.CreateInstance( - typeof(ImGuiBootstrapperConstructionException), - BindingFlags.Instance | BindingFlags.NonPublic, - binder: null, - args: [new InvalidOperationException("partial Silk construction")], - culture: null)!; - using var fixture = new Fixture(bootstrapFailure: bootstrapFailure); - - Assert.Throws(fixture.Compose); - - Assert.True(fixture.Factory.Input!.IsDisposalComplete); - Assert.Null(fixture.Publication.Owner); - Assert.DoesNotContain( - fixture.Logs, - message => message.Contains("devtools disabled", StringComparison.Ordinal)); - } - - [Fact] - public void GameWindowUsesProductionPhaseAndDoesNotRetainHostClosures() - { - string source = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Rendering", - "GameWindow.cs")); - - Assert.Contains("new SettingsDevToolsCompositionPhase(", source, - StringComparison.Ordinal); - Assert.DoesNotContain("new AcDream.UI.ImGui.ImGuiBootstrapper(", source, - StringComparison.Ordinal); - Assert.DoesNotContain("getPlayerPosition: () => GetDebugPlayerPosition()", source, - StringComparison.Ordinal); - Assert.DoesNotContain("_runtimeSettings.ApplyStartup(", source, - StringComparison.Ordinal); - } - - private sealed class Fixture : IDisposable - { - private readonly SettingsDevToolsCompositionPoint? _failurePoint; - private readonly InputDispatcher _dispatcher; - private readonly FrameProfiler _profiler = new(); - - public Fixture( - bool enabled = true, - SettingsDevToolsCompositionPoint? failurePoint = null, - int backendDisposeFailures = 0, - bool inputSupported = true, - Exception? bootstrapFailure = null) - { - _failurePoint = failurePoint; - Factory = new Factory( - backendDisposeFailures, - inputSupported, - bootstrapFailure); - Publication = new Publication(); - Startup = new StartupTarget(); - Settings = new RuntimeSettingsController( - new Storage(), - QualitySettings.From, - static _ => { }); - _dispatcher = InputDispatcher.CreateDetached( - new KeyboardSource(), - new MouseSource(), - new KeyBindings()); - _dispatcher.Attach(); - var camera = new CameraController(new OrbitCamera(), new FlyCamera()); - Host = new HostInputCameraResult( - null!, - null!, - null!, - null!, - null!, - null, - null, - null, - null, - _dispatcher, - camera, - null); - Platform = new GameWindowPlatformResult(TestGameWindowGraphics.OpenGl, null!); - Content = (ContentEffectsAudioResult)RuntimeHelpers.GetUninitializedObject( - typeof(ContentEffectsAudioResult)); - Dependencies = new SettingsDevToolsDependencies( - DispatchProxy.Create(), - Settings, - Startup, - new HostQuiescenceGate(), - GameRuntimeTestFactory.Create(), - enabled - ? new SettingsDevToolsOptionalDependencies( - new Facts(), - new KeyBindingTarget(), - new DeferredCanonicalWorldEntityCountSource(), - new DeferredRenderFrameDiagnosticsSource(), - new DeferredDevToolsPlayerModeCommands()) - : null, - new RuntimeDiagnosticCommandSlot(), - new CombatFeedbackSlot(), - new KeyBindings(), - _profiler, - new FramebufferResizeController(new ViewportAspectState()), - Logs.Add); - } - - public List Points { get; } = []; - public List Logs { get; } = []; - public Factory Factory { get; } - public Publication Publication { get; } - public StartupTarget Startup { get; } - public RuntimeSettingsController Settings { get; } - public SettingsDevToolsDependencies Dependencies { get; } - public HostInputCameraResult Host { get; } - public GameWindowPlatformResult Platform { get; } - public ContentEffectsAudioResult Content { get; } - - public SettingsDevToolsResult Compose() => - new SettingsDevToolsCompositionPhase( - Dependencies, - Publication, - Factory, - point => - { - Points.Add(point); - if (point == _failurePoint) - throw new InvalidOperationException($"fault at {point}"); - }).Compose(Platform, Host, Content); - - public void Dispose() - { - Dependencies.Runtime.Dispose(); - Publication.Owner?.Dispose(); - _dispatcher.Dispose(); - _profiler.Dispose(); - } - } - - private sealed class Publication : IGameWindowSettingsDevToolsPublication - { - public DevToolsCompositionOwner? Owner { get; private set; } - public int PublishCalls { get; private set; } - - public void PublishDevTools(DevToolsCompositionOwner value) - { - if (Owner is not null) - throw new InvalidOperationException("duplicate publication"); - Owner = value; - PublishCalls++; - } - } - - private sealed class Factory( - int backendDisposeFailures, - bool inputSupported, - Exception? bootstrapFailure) - : ISettingsDevToolsCompositionFactory - { - public int Calls { get; private set; } - public InputContext? Input { get; private set; } - public Bootstrap? Bootstrap { get; private set; } - public Backend? Backend { get; private set; } - - public bool IsSupported(IInputContext input) - { - Calls++; - return inputSupported; - } - - public IDevToolsInputContext CreateInputContext( - IInputContext input, - HostQuiescenceGate quiescence) - { - Calls++; - return Input = new InputContext(); - } - - public IImGuiBootstrapper CreateBootstrap( - GL gl, - IView window, - IInputContext input) - { - Calls++; - if (bootstrapFailure is not null) - throw bootstrapFailure; - return Bootstrap = new Bootstrap(); - } - - public ImGuiPanelHost CreatePanelHost() - { - Calls++; - return new ImGuiPanelHost(); - } - - public IDevToolsFrameBackend CreateBackend( - IImGuiBootstrapper bootstrap, - ImGuiPanelHost panels) - { - Calls++; - return Backend = new Backend( - (Bootstrap)bootstrap, - backendDisposeFailures); - } - } - - private sealed class InputContext : IDevToolsInputContext - { - public bool Activated { get; private set; } - public int DisposeCalls { get; private set; } - public bool IsDisposalComplete { get; private set; } - public nint Handle => 0; - public IReadOnlyList Gamepads { get; } = []; - public IReadOnlyList Joysticks { get; } = []; - public IReadOnlyList Keyboards { get; } = []; - public IReadOnlyList Mice { get; } = []; - public IReadOnlyList OtherDevices { get; } = []; -#pragma warning disable CS0067 - public event Action? ConnectionChanged; -#pragma warning restore CS0067 - public void Activate() => Activated = true; - public void Deactivate() => Activated = false; - public void Dispose() - { - DisposeCalls++; - Activated = false; - IsDisposalComplete = true; - } - } - - private sealed class Bootstrap : IImGuiBootstrapper - { - public int DisposeCalls { get; private set; } - public void BeginFrame(float deltaSeconds) { } - public void Render() { } - public void AbortFrame() { } - public void Dispose() => DisposeCalls++; - } - - private sealed class Backend( - Bootstrap bootstrap, - int remainingDisposeFailures) : IDevToolsFrameBackend - { - private int _remainingDisposeFailures = remainingDisposeFailures; - public int DisposeCalls { get; private set; } - public int LayoutCalls { get; private set; } - public void BeginFrame(float deltaSeconds) { } - public void AbortFrame() { } - public bool BeginMainMenuBar() => false; - public void EndMainMenuBar() { } - public bool BeginMenu(string label) => false; - public void EndMenu() { } - public bool MenuItem(string label, string? shortcut = null, bool selected = false) => false; - public void Separator() { } - public void RenderPanels(AcDream.UI.Abstractions.PanelContext context) { } - public void RenderDrawData() { } - public void SetWindowLayout( - string title, - Vector2 position, - Vector2 size, - DevToolsPanelLayoutCondition condition) => LayoutCalls++; - public void Dispose() - { - DisposeCalls++; - if (_remainingDisposeFailures-- > 0) - throw new InvalidOperationException("backend cleanup failed"); - bootstrap.Dispose(); - } - } - - private sealed class StartupTarget : IRuntimeSettingsStartupTarget - { - public int DisplayCalls { get; private set; } - public int AudioCalls { get; private set; } - public void ApplyDisplay(DisplaySettings display) => DisplayCalls++; - public void ApplyAudio(AudioSettings audio) => AudioCalls++; - } - - private sealed class Storage : IRuntimeSettingsStorage - { - public SettingsStore? LayoutStore => null; - public string Location => "memory://settings"; - public DisplaySettings LoadDisplay() => DisplaySettings.Default; - public AudioSettings LoadAudio() => AudioSettings.Default; - public GameplaySettings LoadGameplay() => GameplaySettings.Default; - public ChatSettings LoadChat() => ChatSettings.Default; - public CharacterSettings LoadCharacter(string toonKey) => CharacterSettings.Default; - public void SaveDisplay(DisplaySettings display) { } - public void SaveAudio(AudioSettings audio) { } - public void SaveGameplay(GameplaySettings gameplay) { } - public void SaveChat(ChatSettings chat) { } - public void SaveCharacter(string toonKey, CharacterSettings character) { } - } - - private sealed class KeyBindingTarget : IRuntimeKeyBindingTarget - { - public void Apply(KeyBindings bindings) { } - } - - private sealed class Facts : IDevToolsRuntimeFacts - { - public Vector3 PlayerPosition => default; - public float PlayerHeadingDegrees => 0; - public uint PlayerCellId => 0; - public bool PlayerOnGround => false; - public bool InPlayerMode => false; - public bool InFlyMode => false; - public float VerticalVelocity => 0; - public int EntityCount => 0; - public int AnimatedCount => 0; - public int VisibleLandblocks => 0; - public int TotalLandblocks => 0; - public int ShadowObjectCount => 0; - public float NearestObjectDistance => float.PositiveInfinity; - public string NearestObjectLabel => "-"; - public bool Colliding => false; - public bool CollisionWireframesVisible => false; - public int StreamingRadius => 0; - public float MouseSensitivity => 1; - public float ChaseDistance => 0; - public bool RmbOrbitHeld => false; - public string HourName => "0"; - public float DayFraction => 0; - public string Weather => "Clear"; - public int ActiveLights => 0; - public int RegisteredLights => 0; - public int ParticleCount => 0; - public float Fps => 60; - public float FrameMilliseconds => 16.7f; - } - - private sealed class KeyboardSource : IKeyboardSource - { -#pragma warning disable CS0067 - public event Action? KeyDown; - public event Action? KeyUp; -#pragma warning restore CS0067 - public bool IsHeld(Key key) => false; - public ModifierMask CurrentModifiers => ModifierMask.None; - } - - private sealed class MouseSource : IMouseSource - { -#pragma warning disable CS0067 - public event Action? MouseDown; - public event Action? MouseUp; - public event Action? MouseMove; - public event Action? Scroll; -#pragma warning restore CS0067 - public bool IsHeld(MouseButton button) => false; - public bool WantCaptureMouse => false; - public bool WantCaptureKeyboard => false; - } - - public class ViewProxy : DispatchProxy - { - protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) - { - if (targetMethod?.Name == "get_Size") - return new Vector2D(1280, 720); - Type type = targetMethod?.ReturnType ?? typeof(void); - if (type == typeof(void)) - return null; - return type.IsValueType ? Activator.CreateInstance(type) : null; - } - } - - private static string FindRepoRoot() - { - DirectoryInfo? directory = new(AppContext.BaseDirectory); - while (directory is not null) - { - if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx"))) - return directory.FullName; - directory = directory.Parent; - } - throw new DirectoryNotFoundException("Could not find AcDream.slnx."); - } -} diff --git a/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs b/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs index 30b3ba58..5ff43550 100644 --- a/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs @@ -196,8 +196,7 @@ public sealed class WorldRenderCompositionTests new GameWindowPlatformResult(TestGameWindowGraphics.OpenGl, null!), Content, new SettingsDevToolsResult( - QualitySettings.From(QualityPreset.High), - null)); + QualitySettings.From(QualityPreset.High))); } private sealed class RenderLifetime(TerrainAtlas atlas) diff --git a/tests/AcDream.App.Tests/Platform/GraphicalHostPlatformServicesTests.cs b/tests/AcDream.App.Tests/Platform/GraphicalHostPlatformServicesTests.cs index 8e80f3a8..4428c575 100644 --- a/tests/AcDream.App.Tests/Platform/GraphicalHostPlatformServicesTests.cs +++ b/tests/AcDream.App.Tests/Platform/GraphicalHostPlatformServicesTests.cs @@ -12,7 +12,9 @@ public sealed class GraphicalHostPlatformServicesTests Assert.NotNull(platform.Paths); Assert.NotNull(platform.FramePacingWaiters); - Assert.Equal(3, platform.NativeDependencies.Count); + // Campaign V slice V11 removed the ImGui developer-tools frontend and + // its cimgui native dependency, leaving window/input + audio. + Assert.Equal(2, platform.NativeDependencies.Count); Assert.All( platform.NativeDependencies, dependency => diff --git a/tests/AcDream.App.Tests/Rendering/DevToolsFramePresenterTests.cs b/tests/AcDream.App.Tests/Rendering/DevToolsFramePresenterTests.cs deleted file mode 100644 index 4895eb4b..00000000 --- a/tests/AcDream.App.Tests/Rendering/DevToolsFramePresenterTests.cs +++ /dev/null @@ -1,455 +0,0 @@ -using System.Numerics; -using System.Reflection; -using AcDream.App.Diagnostics; -using AcDream.App.Net; -using AcDream.App.Rendering; -using AcDream.UI.Abstractions; - -namespace AcDream.App.Tests.Rendering; - -public sealed class DevToolsFramePresenterTests -{ - [Fact] - public void CommandBusOwnedBindingReleasesExactlyAndAllowsRebind() - { - var source = new DevToolsCommandBusSource(); - var first = new TestUiSessionTarget(); - var second = new TestUiSessionTarget(); - - IDisposable firstBinding = source.BindOwned(first); - Assert.Same(first.Commands, source.Current); - Assert.Throws(() => source.BindOwned(second)); - - firstBinding.Dispose(); - using IDisposable secondBinding = source.BindOwned(second); - firstBinding.Dispose(); - - Assert.Same(second.Commands, source.Current); - } - - private sealed class TestUiSessionTarget : ILiveUiSessionTarget - { - public bool IsInWorld => false; - public AcDream.Core.Net.WorldSession? CurrentSession => null; - public ICommandBus Commands { get; } = new RecordingCommandBus(); - } - - [Fact] - public void Frame_PreservesBeginThenMenuPanelsAndDrawDataOrder() - { - var calls = new List(); - var backend = new RecordingBackend(calls); - var commands = new RecordingCommandSource(); - var presenter = Create(backend, commands: commands); - - presenter.BeginFrame(0.25f); - presenter.Render(0.5, 1280, 720); - - Assert.Equal( - [ - "begin:0.25", - "begin-bar", - "begin-menu:View", - "begin-menu:Camera", - "end-bar", - "panels:0.5", - "draw-data", - ], - calls); - Assert.Same(commands.Current, backend.Contexts.Single().Commands); - } - - [Fact] - public void Render_ResolvesTheCurrentCommandBusEveryFrame() - { - var backend = new RecordingBackend([]); - var first = new RecordingCommandBus(); - var second = new RecordingCommandBus(); - var commands = new RecordingCommandSource { Current = first }; - var presenter = Create(backend, commands: commands); - - presenter.BeginFrame(0.1f); - presenter.Render(0.1, 800, 600); - commands.Current = second; - presenter.BeginFrame(0.2f); - presenter.Render(0.2, 800, 600); - - Assert.Same(first, backend.Contexts[0].Commands); - Assert.Same(second, backend.Contexts[1].Commands); - } - - [Fact] - public void ViewMenu_TogglesExactPanelsAndUsesCurrentViewportForReset() - { - var backend = new RecordingBackend([]); - backend.OpenMenus.Add("View"); - backend.ClickedItems.UnionWith( - ["Settings", "Vitals", "Chat", "Debug", "Reset window layout"]); - var panels = new RecordingPanels(); - var presenter = Create(backend, panels: panels); - - presenter.BeginFrame(0.1f); - presenter.Render(0.1, 1920, 1080); - - Assert.Equal( - [ - DevToolsPanelKind.Settings, - DevToolsPanelKind.Vitals, - DevToolsPanelKind.Chat, - DevToolsPanelKind.Debug, - ], - panels.Toggles); - Assert.Equal( - [ - new LayoutCall("Vitals", new Vector2(10f, 30f), new Vector2(220f, 110f)), - new LayoutCall("Chat", new Vector2(10f, 760f), new Vector2(450f, 300f)), - new LayoutCall("Debug", new Vector2(1540f, 30f), new Vector2(370f, 520f)), - new LayoutCall("Settings", new Vector2(610f, 290f), new Vector2(700f, 500f)), - ], - backend.Layouts.Select(call => call.WithoutCondition())); - Assert.All( - backend.Layouts, - call => Assert.Equal(DevToolsPanelLayoutCondition.Always, call.Condition)); - } - - [Fact] - public void CameraMenu_UsesFlyLabelAndRoundTripsCollisionState() - { - var backend = new RecordingBackend([]); - backend.OpenMenus.Add("Camera"); - backend.ClickedItems.UnionWith( - ["Exit Free-Fly Mode", "Collide Camera (spring arm)"]); - var camera = new RecordingCamera { IsFlyMode = true, CollideCamera = true }; - var presenter = Create(backend, camera: camera); - - presenter.BeginFrame(0.1f); - presenter.Render(0.1, 1280, 720); - - Assert.Equal(1, camera.ToggleCount); - Assert.False(camera.CollideCamera); - Assert.Contains( - backend.MenuItems, - item => item.Label == "Collide Camera (spring arm)" && item.Selected); - } - - [Fact] - public void MissingSettingsPanel_IsInertAndSkippedFromLayout() - { - var backend = new RecordingBackend([]); - backend.OpenMenus.Add("View"); - backend.ClickedItems.Add("Settings"); - var panels = new RecordingPanels { HasSettings = false }; - var presenter = Create(backend, panels: panels); - - presenter.ToggleSettingsPanel(); - presenter.ResetLayout(1280, 720, DevToolsPanelLayoutCondition.FirstUseEver); - presenter.BeginFrame(0.1f); - presenter.Render(0.1, 1280, 720); - - Assert.DoesNotContain(DevToolsPanelKind.Settings, panels.Toggles); - Assert.DoesNotContain(backend.MenuItems, item => item.Label == "Settings"); - Assert.DoesNotContain(backend.Layouts, call => call.Title == "Settings"); - } - - [Theory] - [InlineData(1280, 720, 900, 400, 290, 110)] - [InlineData(1920, 1080, 1540, 760, 610, 290)] - [InlineData(100, 100, 100, 0, -110, -90)] - public void ResetLayout_PreservesExactFormulasAndMinimumViewport( - int width, - int height, - int debugX, - int chatY, - int settingsX, - int settingsY) - { - var backend = new RecordingBackend([]); - var presenter = Create(backend); - - presenter.ResetLayout(width, height, DevToolsPanelLayoutCondition.FirstUseEver); - - Assert.Collection( - backend.Layouts, - call => Assert.Equal( - new LayoutCall("Vitals", new Vector2(10f, 30f), new Vector2(220f, 110f)), - call.WithoutCondition()), - call => Assert.Equal( - new LayoutCall("Chat", new Vector2(10f, chatY), new Vector2(450f, 300f)), - call.WithoutCondition()), - call => Assert.Equal( - new LayoutCall("Debug", new Vector2(debugX, 30f), new Vector2(370f, 520f)), - call.WithoutCondition()), - call => Assert.Equal( - new LayoutCall( - "Settings", - new Vector2(settingsX, settingsY), - new Vector2(700f, 500f)), - call.WithoutCondition())); - } - - [Fact] - public void AbortFrame_ClosesAnOpenBackendFrameAndAllowsTheNextFrame() - { - var calls = new List(); - var backend = new RecordingBackend(calls); - var presenter = Create(backend); - - presenter.BeginFrame(0.1f); - presenter.AbortFrame(); - presenter.AbortFrame(); - presenter.BeginFrame(0.2f); - presenter.Render(0.2, 800, 600); - - Assert.Equal(1, calls.Count(call => call == "abort")); - Assert.Equal(2, calls.Count(call => call.StartsWith("begin:"))); - Assert.Equal(1, calls.Count(call => call == "draw-data")); - } - - [Fact] - public void RenderFailure_RemainsAbortableAndDoesNotPoisonTheNextFrame() - { - var calls = new List(); - var backend = new RecordingBackend(calls) - { - DrawFailure = new InvalidOperationException("draw"), - }; - var presenter = Create(backend); - - presenter.BeginFrame(0.1f); - Assert.Throws(() => - presenter.Render(0.1, 800, 600)); - presenter.AbortFrame(); - - backend.DrawFailure = null; - presenter.BeginFrame(0.2f); - presenter.Render(0.2, 800, 600); - - Assert.Equal(0, calls.Count(call => call == "abort")); - Assert.Equal(2, calls.Count(call => call == "draw-data")); - } - - [Fact] - public void BeginFailure_RemainsAbortableAndDoesNotPoisonTheNextFrame() - { - var calls = new List(); - var backend = new RecordingBackend(calls) - { - BeginFailure = new InvalidOperationException("begin"), - }; - var presenter = Create(backend); - - Assert.Throws(() => presenter.BeginFrame(0.1f)); - presenter.AbortFrame(); - - backend.BeginFailure = null; - presenter.BeginFrame(0.2f); - presenter.Render(0.2, 800, 600); - - Assert.Equal(1, calls.Count(call => call == "abort")); - Assert.Equal(2, calls.Count(call => call.StartsWith("begin:"))); - } - - [Fact] - public void InputFacingActionsDelegateToTheTypedPanelOwner() - { - var panels = new RecordingPanels(); - var presenter = Create(new RecordingBackend([]), panels: panels); - - presenter.ToggleDebugPanel(); - presenter.FocusChatInput(); - presenter.ToggleSettingsPanel(); - - Assert.Equal( - [DevToolsPanelKind.Debug, DevToolsPanelKind.Settings], - panels.Toggles); - Assert.Equal(1, panels.FocusChatCount); - Assert.Equal(["Debug panel ON"], panels.Toasts); - } - - [Fact] - public void PresenterAndAdaptersHaveNoWindowOrDelegateBackReferences() - { - Type[] owners = - [ - typeof(DevToolsFramePresenter), - typeof(ImGuiDevToolsFrameBackend), - typeof(DevToolsCameraMenuOperations), - typeof(DevToolsCommandBusSource), - typeof(DevToolsPanelSet), - ]; - - foreach (Type owner in owners) - { - FieldInfo[] fields = owner.GetFields( - BindingFlags.Instance | BindingFlags.NonPublic); - Assert.DoesNotContain(fields, field => field.FieldType == typeof(GameWindow)); - Assert.DoesNotContain( - fields, - field => typeof(Delegate).IsAssignableFrom(field.FieldType)); - Assert.DoesNotContain( - fields, - field => field.FieldType == typeof(Silk.NET.Windowing.IWindow)); - } - - Assert.True(typeof(IDisposable).IsAssignableFrom( - typeof(ImGuiDevToolsFrameBackend))); - } - - private static DevToolsFramePresenter Create( - RecordingBackend backend, - RecordingCamera? camera = null, - RecordingCommandSource? commands = null, - RecordingPanels? panels = null) => - new( - backend, - camera ?? new RecordingCamera(), - commands ?? new RecordingCommandSource(), - new FrameProfiler(), - panels ?? new RecordingPanels()); - - private sealed class RecordingBackend(List calls) : IDevToolsFrameBackend - { - public void Dispose() { } - public HashSet OpenMenus { get; } = []; - public HashSet ClickedItems { get; } = []; - public List<(string Label, string? Shortcut, bool Selected)> MenuItems { get; } = []; - public List Contexts { get; } = []; - public List Layouts { get; } = []; - public Exception? BeginFailure { get; set; } - public Exception? DrawFailure { get; set; } - - public bool ControllerFrameOpen { get; private set; } - - public void BeginFrame(float deltaSeconds) - { - calls.Add($"begin:{deltaSeconds}"); - ControllerFrameOpen = true; - if (BeginFailure is not null) - throw BeginFailure; - } - - public void AbortFrame() - { - // Silk ImGuiController.Render is idempotent. It closes an active - // controller frame, but is a no-op if Render already cleared its - // private _frameBegun flag before a GL upload failure. - if (!ControllerFrameOpen) - return; - ControllerFrameOpen = false; - calls.Add("abort"); - } - - public bool BeginMainMenuBar() - { - calls.Add("begin-bar"); - return true; - } - - public void EndMainMenuBar() => calls.Add("end-bar"); - - public bool BeginMenu(string label) - { - calls.Add($"begin-menu:{label}"); - return OpenMenus.Contains(label); - } - - public void EndMenu() => calls.Add("end-menu"); - - public bool MenuItem(string label, string? shortcut = null, bool selected = false) - { - MenuItems.Add((label, shortcut, selected)); - return ClickedItems.Contains(label); - } - - public void Separator() => calls.Add("separator"); - - public void RenderPanels(PanelContext context) - { - Contexts.Add(context); - calls.Add($"panels:{context.DeltaSeconds}"); - } - - public void RenderDrawData() - { - Assert.True(ControllerFrameOpen); - ControllerFrameOpen = false; - calls.Add("draw-data"); - if (DrawFailure is not null) - throw DrawFailure; - } - - public void SetWindowLayout( - string title, - Vector2 position, - Vector2 size, - DevToolsPanelLayoutCondition condition) => - Layouts.Add(new LayoutCallWithCondition(title, position, size, condition)); - } - - private sealed class RecordingCamera : IDevToolsCameraMenuOperations - { - public bool IsFlyMode { get; init; } - public bool CollideCamera { get; set; } - public int ToggleCount { get; private set; } - - public void ToggleFlyOrChase() => ToggleCount++; - } - - private sealed class RecordingCommandSource : IDevToolsCommandBusSource - { - public ICommandBus Current { get; set; } = new RecordingCommandBus(); - } - - private sealed class RecordingCommandBus : ICommandBus - { - public void Publish(T command) where T : notnull - { - } - } - - private sealed class RecordingPanels : IDevToolsPanelSet - { - private readonly HashSet _visible = []; - - public bool HasSettings { get; init; } = true; - public List Toggles { get; } = []; - public List Toasts { get; } = []; - public int FocusChatCount { get; private set; } - - public bool Contains(DevToolsPanelKind kind) => - kind != DevToolsPanelKind.Settings || HasSettings; - - public string? GetTitle(DevToolsPanelKind kind) => Contains(kind) - ? kind.ToString() - : null; - - public bool IsVisible(DevToolsPanelKind kind) => _visible.Contains(kind); - - public void Toggle(DevToolsPanelKind kind) - { - if (!Contains(kind)) - return; - Toggles.Add(kind); - if (!_visible.Add(kind)) - _visible.Remove(kind); - } - - public void FocusChatInput() => FocusChatCount++; - - public void AddDebugToast(string message) => Toasts.Add(message); - } - - private readonly record struct LayoutCall( - string Title, - Vector2 Position, - Vector2 Size); - - private readonly record struct LayoutCallWithCondition( - string Title, - Vector2 Position, - Vector2 Size, - DevToolsPanelLayoutCondition Condition) - { - public LayoutCall WithoutCondition() => new(Title, Position, Size); - } -} diff --git a/tests/AcDream.App.Tests/Rendering/GameWindowRenderLeafCompositionTests.cs b/tests/AcDream.App.Tests/Rendering/GameWindowRenderLeafCompositionTests.cs index 6101bf90..174c3bb1 100644 --- a/tests/AcDream.App.Tests/Rendering/GameWindowRenderLeafCompositionTests.cs +++ b/tests/AcDream.App.Tests/Rendering/GameWindowRenderLeafCompositionTests.cs @@ -108,13 +108,6 @@ public sealed class GameWindowRenderLeafCompositionTests Assert.DoesNotContain(identifier, source, StringComparison.Ordinal); Assert.Contains("new PaperdollFramePresenter(", LivePresentationSource()); - string settingsComposition = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "SettingsDevToolsComposition.cs")); - Assert.Contains("new DevToolsFramePresenter(", settingsComposition); string framePhase = FrameRootSource(); Assert.Contains("new RenderFrameResourceController(", framePhase); Assert.Contains("new RenderWeatherFrameController(", framePhase); @@ -131,23 +124,21 @@ public sealed class GameWindowRenderLeafCompositionTests } [Fact] - public void Shutdown_PreservesBorrowedDevtoolsLifetimeAndDrainsGpuBeforeFrontends() + public void Shutdown_DrainsGpuBeforeFrontendsAndPreservesFrameBorrowerOrder() { + // Campaign V slice V11 removed the ImGui developer-tools frontend and + // its "developer tools" shutdown stage entry along with it; this test + // used to pin that entry's position and is now renamed to pin what + // survives it. string source = GameWindowLifetimeSource(); AssertAppearsInOrder( source, "new ResourceShutdownStage(\"submitted GPU work\"", "new ResourceShutdownStage(\"render frontends\"", - "Hard(\"developer tools\", () => DisposeDevToolsFrontend(render.DevTools))", "Hard(\"portal tunnel\"", "Hard(\"paperdoll viewport\"", "new ResourceShutdownStage(\"OpenGL context\""); - AssertAppearsInOrder( - source, - "private static void DisposeDevToolsFrontend(DevToolsCompositionOwner? owner)", - "owner.DisposeFrontend()", - "if (!owner.IsFrontendDisposalComplete)"); AssertAppearsInOrder( source, "new ResourceShutdownStage(\"frame borrowers\"", @@ -161,7 +152,7 @@ public sealed class GameWindowRenderLeafCompositionTests AssertAppearsInOrder( source, "frame.FrameGraphPublication?.Dispose()", - "Hard(\"developer tools\", () => DisposeDevToolsFrontend(render.DevTools))", + "new ResourceShutdownStage(\"render frontends\"", "new ResourceShutdownStage(\"input context\"", "platform.Input?.Dispose()", "new ResourceShutdownStage(\"OpenGL context\""); diff --git a/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs b/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs index bc949b59..f00dc6a4 100644 --- a/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs +++ b/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs @@ -58,7 +58,7 @@ public sealed class GameWindowSlice8BoundaryTests "new ContentEffectsAudioCompositionPhase(", "this).Compose(platformResult, hostInputCamera),", "new SettingsDevToolsCompositionPhase(", - "this).Compose(platformResult, hostInputCamera, contentEffectsAudio);", + ".Compose(platformResult, hostInputCamera, contentEffectsAudio),", "new InteractionRetainedUiCompositionPhase(", "this).Compose(", "new LivePresentationCompositionPhase(", @@ -197,17 +197,6 @@ public sealed class GameWindowSlice8BoundaryTests Assert.DoesNotContain("private void CycleTimeOfDay()", source, StringComparison.Ordinal); Assert.DoesNotContain("private void CycleWeather()", source, StringComparison.Ordinal); - string settingsPhase = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "SettingsDevToolsComposition.cs")); - AssertAppearsInOrder( - settingsPhase, - "debugVm.CycleTimeOfDay = _dependencies.DiagnosticCommands.CycleTimeOfDay;", - "debugVm.CycleWeather = _dependencies.DiagnosticCommands.CycleWeather;", - "debugVm.ToggleCollisionWires ="); string sessionPhase = File.ReadAllText(Path.Combine( FindRepoRoot(), "src", @@ -349,7 +338,7 @@ public sealed class GameWindowSlice8BoundaryTests load, "GameWindowCompositionPipeline.Run<", "new SettingsDevToolsCompositionPhase(", - "this).Compose(platformResult, hostInputCamera, contentEffectsAudio);", + ".Compose(platformResult, hostInputCamera, contentEffectsAudio),", "new WorldRenderCompositionPhase(", "new SessionPlayerCompositionPhase(", "new FrameRootCompositionPhase(", @@ -371,10 +360,10 @@ public sealed class GameWindowSlice8BoundaryTests "AcDream.App", "Composition", "SettingsDevToolsComposition.cs")); - AssertAppearsInOrder( - settingsPhase, + Assert.Contains( "_dependencies.Settings.ApplyStartup(_dependencies.StartupTarget);", - "_dependencies.DevTools is { } optional"); + settingsPhase, + StringComparison.Ordinal); string worldPhase = File.ReadAllText(Path.Combine( FindRepoRoot(), "src", diff --git a/tests/AcDream.App.Tests/Rendering/PrivatePresentationRendererTests.cs b/tests/AcDream.App.Tests/Rendering/PrivatePresentationRendererTests.cs index 802e858f..de878f73 100644 --- a/tests/AcDream.App.Tests/Rendering/PrivatePresentationRendererTests.cs +++ b/tests/AcDream.App.Tests/Rendering/PrivatePresentationRendererTests.cs @@ -162,9 +162,6 @@ public sealed class PrivatePresentationRendererTests Assert.Contains( typeof(AcDream.App.UI.UiElement), FieldTypes(typeof(PaperdollInventoryVisibility))); - Assert.Contains( - typeof(AcDream.UI.Abstractions.Panels.Debug.DebugVM), - FieldTypes(typeof(DevToolsPanelSet))); Assert.Contains( typeof(AcDream.App.Settings.IRuntimeSettingsPreviewSource), FieldTypes(typeof(RuntimeWorldFrameSettingsPreview))); diff --git a/tests/AcDream.App.Tests/Studio/CanvasCoordMappingTests.cs b/tests/AcDream.App.Tests/Studio/CanvasCoordMappingTests.cs deleted file mode 100644 index 9d007bf3..00000000 --- a/tests/AcDream.App.Tests/Studio/CanvasCoordMappingTests.cs +++ /dev/null @@ -1,118 +0,0 @@ -using AcDream.App.Studio; - -namespace AcDream.App.Tests.Studio; - -/// -/// Pure-math tests for the canvas → panel-local coordinate mapping used by -/// . -/// -/// No GL context required — we're just verifying the formula: -/// panel_local = (raw_mouse_screen) - (image_screen_top_left) -/// -/// The image_screen_top_left is what ImGui.GetItemRectMin() returns after -/// ImGui.Image: the sub-window top-left + title-bar height + inner padding + any -/// scrolling. We model it as a constant offset in these tests. -/// -/// V-flip: the image is drawn with uv0=(0,1) / uv1=(1,0) so GL's bottom-left -/// origin is flipped to top-left on screen. After the flip, screen Y=0 (top of image) -/// = panel Y=0 (top of the UI), so NO additional Y inversion is applied. -/// -public class CanvasCoordMappingTests -{ - // Simulates the mapping DrawCanvas performs: - // panel pixel = mouse_screen - image_rectMin - // Returns null when the result falls outside [0, width) x [0, height). - private static (int px, int py)? Map( - float mouseScreenX, float mouseScreenY, - float imageOriginX, float imageOriginY, - int panelWidth, int panelHeight) - { - int ix = (int)(mouseScreenX - imageOriginX); - int iy = (int)(mouseScreenY - imageOriginY); - if (ix < 0 || ix >= panelWidth || iy < 0 || iy >= panelHeight) - return null; - return (ix, iy); - } - - [Fact] - public void TopLeft_of_image_maps_to_panel_origin() - { - // The canvas image starts at screen (300, 50) (after sub-window chrome). - // A click exactly at the image's screen top-left → panel (0, 0). - var result = Map(mouseScreenX: 300f, mouseScreenY: 50f, - imageOriginX: 300f, imageOriginY: 50f, - panelWidth: 1280, panelHeight: 720); - Assert.Equal((0, 0), result); - } - - [Fact] - public void Interior_point_maps_correctly() - { - // Image origin at screen (300, 50). Mouse at screen (780, 230). - // Expected panel coord: (780-300, 230-50) = (480, 180). - var result = Map(mouseScreenX: 780f, mouseScreenY: 230f, - imageOriginX: 300f, imageOriginY: 50f, - panelWidth: 1280, panelHeight: 720); - Assert.Equal((480, 180), result); - } - - [Fact] - public void Bottom_right_corner_maps_to_last_valid_pixel() - { - // Image is 1280×720, origin at screen (300, 50). - // Last pixel in bottom-right is panel (1279, 719) → screen (1579, 769). - var result = Map(mouseScreenX: 1579f, mouseScreenY: 769f, - imageOriginX: 300f, imageOriginY: 50f, - panelWidth: 1280, panelHeight: 720); - Assert.Equal((1279, 719), result); - } - - [Fact] - public void Mouse_on_ImGui_chrome_above_image_returns_null() - { - // The ImGui window title-bar / padding is above rectMin, i.e. at screenY < 50. - // The mouse there should NOT produce a panel event. - var result = Map(mouseScreenX: 400f, mouseScreenY: 40f, // 10px above image origin - imageOriginX: 300f, imageOriginY: 50f, - panelWidth: 1280, panelHeight: 720); - Assert.Null(result); - } - - [Fact] - public void Mouse_below_image_returns_null() - { - // screenY = 50 + 720 = 770 → iy = 720 which is >= panelHeight (720). - var result = Map(mouseScreenX: 400f, mouseScreenY: 770f, - imageOriginX: 300f, imageOriginY: 50f, - panelWidth: 1280, panelHeight: 720); - Assert.Null(result); - } - - [Fact] - public void Y_is_not_inverted_after_vflip() - { - // Confirm the "no extra Y inversion" contract: - // The image is V-flipped in ImGui (uv0.Y=1, uv1.Y=0), so screen top row = panel Y=0. - // A click near the TOP of the image should give a SMALL panel Y, not a large one. - // Image origin at (300, 50). Click at (400, 55) → panel (100, 5). Y is small (near top). - var result = Map(mouseScreenX: 400f, mouseScreenY: 55f, - imageOriginX: 300f, imageOriginY: 50f, - panelWidth: 1280, panelHeight: 720); - Assert.Equal((100, 5), result); - // NOT (100, 715) — which would be the result if Y were incorrectly inverted. - Assert.True(result!.Value.py < 720 / 2, "Y near screen top should map to small panel Y, not near bottom"); - } - - [Fact] - public void LargeChrome_offset_is_fully_absorbed() - { - // Simulate a canvas sub-window with large chrome: ImGui title (20px) + padding (8px) - // puts the image origin at screenY = menuBar(22) + titleBar(20) + padding(8) = 50. - // Also a wide tree pane puts imageOriginX = 280 + padding. - // A click at screen (400, 110) with origin (290, 50) → panel (110, 60). - var result = Map(mouseScreenX: 400f, mouseScreenY: 110f, - imageOriginX: 290f, imageOriginY: 50f, - panelWidth: 1280, panelHeight: 720); - Assert.Equal((110, 60), result); - } -} diff --git a/tests/AcDream.App.Tests/Studio/DumpLayoutTests.cs b/tests/AcDream.App.Tests/Studio/DumpLayoutTests.cs deleted file mode 100644 index 2b635778..00000000 --- a/tests/AcDream.App.Tests/Studio/DumpLayoutTests.cs +++ /dev/null @@ -1,188 +0,0 @@ -using AcDream.App.Studio; -using AcDream.App.UI; - -namespace AcDream.App.Tests.Studio; - -/// -/// Tests for — parsing the retail UI dump JSON and -/// building a tree from it. -/// -/// These tests load the real dump file from the source tree -/// (docs/research/2026-06-25-retail-ui-layout-dump.json). The test -/// skips cleanly when the file is absent (should not happen in a normal dev -/// checkout, but guards against stripped CI machines). -/// -public class DumpLayoutTests -{ - private static string DumpPath() - { - // Walk up from the test output directory to the solution root, - // mirroring ConformanceDats.SolutionRoot(). - var dir = AppContext.BaseDirectory; - while (!string.IsNullOrEmpty(dir)) - { - if (File.Exists(Path.Combine(dir, "AcDream.slnx"))) - return Path.Combine(dir, "docs", "research", - "2026-06-25-retail-ui-layout-dump.json"); - dir = Path.GetDirectoryName(dir); - } - // Fallback: try a relative path (won't find it but skip rather than throw) - return Path.Combine(AppContext.BaseDirectory, - "docs", "research", "2026-06-25-retail-ui-layout-dump.json"); - } - - private static (uint, int, int) NoTex(uint _) => (1u, 1, 1); - - // ── Helpers ────────────────────────────────────────────────────────── - - /// Depth-first search for an element with the given EventId. - private static UiElement? FindById(UiElement root, uint id) - { - if (root.EventId == id) return root; - foreach (var c in root.Children) - { - var found = FindById(c, id); - if (found is not null) return found; - } - return null; - } - - /// Count the total elements in the tree (self + all descendants). - private static int CountAll(UiElement root) - { - int n = 1; - foreach (var c in root.Children) n += CountAll(c); - return n; - } - - // ── Tests ───────────────────────────────────────────────────────────── - - /// - /// Loading the "inventory" slug should succeed and the returned tree should - /// contain an element with EventId == 0x100001D5 (the doll viewport node) - /// and at least 40 elements in total. - /// - [Fact] - public void Load_Inventory_ReturnsTreeWithDollViewport() - { - var path = DumpPath(); - if (!File.Exists(path)) - return; // Skip: dump not available. - - var root = DumpLayout.Load(path, "inventory", NoTex, out var err); - - Assert.NotNull(root); - Assert.Null(err); - - // The doll viewport element must appear somewhere in the tree. - const uint dollViewportId = 0x100001D5u; - var found = FindById(root!, dollViewportId); - Assert.NotNull(found); - - // The full tree must be reasonably deep — 59 dump nodes → >= 40 elements. - int total = CountAll(root!); - Assert.True(total >= 40, - $"Expected >= 40 elements in inventory tree; got {total}"); - } - - /// - /// Loading an unknown slug must return null and a non-empty error string. - /// - [Fact] - public void Load_UnknownSlug_ReturnsNullWithError() - { - var path = DumpPath(); - if (!File.Exists(path)) - return; // Skip. - - var root = DumpLayout.Load(path, "this_slug_does_not_exist", NoTex, out var err); - - Assert.Null(root); - Assert.NotNull(err); - Assert.NotEmpty(err!); - } - - /// - /// The root element's Left/Top should be (0,0) (the panel's rect offset has - /// been stripped so the tree sits at the window origin), and its Width/Height - /// should match the dump panel dimensions. - /// - [Fact] - public void Load_Inventory_RootAtOrigin() - { - var path = DumpPath(); - if (!File.Exists(path)) - return; // Skip. - - var root = DumpLayout.Load(path, "inventory", NoTex, out _); - Assert.NotNull(root); - - // Root always placed at (0,0) by DumpLayout (origin of the UiHost). - Assert.Equal(0f, root!.Left); - Assert.Equal(0f, root.Top); - // Width/Height come from the panel record in the dump. - Assert.True(root.Width > 0, "Root width must be > 0"); - Assert.True(root.Height > 0, "Root height must be > 0"); - } - - /// - /// Children must use parent-relative coordinates (the dump rects are absolute; - /// DumpLayout subtracts the parent rect to produce parent-local offsets). - /// Verify that at least the direct children of the root have Left/Top values - /// that are NOT equal to the absolute rect they had in the dump (since the root - /// was at x>0 in screen space but we place it at 0,0). - /// - [Fact] - public void Load_Inventory_ChildrenAreParentRelative() - { - var path = DumpPath(); - if (!File.Exists(path)) - return; // Skip. - - var root = DumpLayout.Load(path, "inventory", NoTex, out _); - Assert.NotNull(root); - - // If the dump has children at absolute x>=500 but the root is at 0, - // a correct parent-relative placement will give children x < 500. - // (The inventory panel root is at absolute x=500; children in the dump - // also start at x=500 — after subtraction they should land near x=0.) - bool anyChildAtAbsoluteX = false; - foreach (var child in root!.Children) - { - if (child.Left >= 490f) // would indicate absolute not relative - { - anyChildAtAbsoluteX = true; - break; - } - } - Assert.False(anyChildAtAbsoluteX, - "Children appear to have absolute coords (Left >= 490) — " + - "DumpLayout must subtract the parent rect."); - } - - /// - /// Every panel slug known in the dump must load without error. - /// This is a smoke test that the JSON parse + tree build does not - /// crash on any of the 26 panels. - /// - [Fact] - public void Load_AllSlugs_Succeed() - { - var path = DumpPath(); - if (!File.Exists(path)) - return; // Skip. - - var slugs = UiDumpModel.ListSlugs(path); - Assert.True(slugs.Count >= 20, - $"Expected >= 20 panel slugs in dump; got {slugs.Count}"); - - foreach (var slug in slugs) - { - var root = DumpLayout.Load(path, slug, NoTex, out var err); - Assert.True(root is not null || err is not null, - $"Load('{slug}') returned both null root AND null error — one must be set."); - if (root is null) - Assert.Fail($"Slug '{slug}' failed with error: {err}"); - } - } -} diff --git a/tests/AcDream.App.Tests/Studio/FixtureProviderTests.cs b/tests/AcDream.App.Tests/Studio/FixtureProviderTests.cs deleted file mode 100644 index 32794876..00000000 --- a/tests/AcDream.App.Tests/Studio/FixtureProviderTests.cs +++ /dev/null @@ -1,160 +0,0 @@ -using AcDream.App.Studio; -using AcDream.Core.Items; - -namespace AcDream.App.Tests.Studio; - -/// -/// Unit tests for and . -/// These tests have NO GL/dat dependency — they only exercise the in-memory -/// ClientObjectTable population that FixtureProvider uses. -/// -public class FixtureProviderTests -{ - /// - /// SampleData.BuildObjectTable() must place at least 6 items in the - /// player's main pack (ContainerId == PlayerGuid) and the player object - /// itself must exist with the right capacities. - /// - [Fact] - public void SampleTable_hasPackContents() - { - var t = SampleData.BuildObjectTable(); - - // Player object must exist. - var player = t.Get(SampleData.PlayerGuid); - Assert.NotNull(player); - Assert.Equal(102, player!.ItemsCapacity); - Assert.Equal(7, player.ContainersCapacity); - - // At least 6 loose items must be in the main pack. - var contents = t.GetContents(SampleData.PlayerGuid); - Assert.True(contents.Count >= 6, - $"Expected >= 6 items in player pack; got {contents.Count}"); - - // Verify the first item has a non-zero IconId and a recognised Type. - var firstId = contents[0]; - var first = t.Get(firstId); - Assert.NotNull(first); - Assert.NotEqual(0u, first!.IconId); - Assert.NotEqual(ItemType.None, first.Type); - } - - /// - /// Spot-check that the sample table also seeds a sword-like item - /// (MeleeWeapon) and a piece of armor so icon resolution has - /// recognisable item types to work with. - /// - [Fact] - public void SampleTable_hasWeaponAndArmor() - { - var t = SampleData.BuildObjectTable(); - var contents = t.GetContents(SampleData.PlayerGuid); - - bool hasMelee = false, hasArmor = false; - foreach (var guid in contents) - { - var obj = t.Get(guid); - if (obj is null) continue; - if ((obj.Type & ItemType.MeleeWeapon) != 0) hasMelee = true; - if ((obj.Type & ItemType.Armor) != 0) hasArmor = true; - } - - Assert.True(hasMelee, "Expected at least one MeleeWeapon in sample pack"); - Assert.True(hasArmor, "Expected at least one Armor item in sample pack"); - } - - /// - /// Sample table must include at least 1 equipped item whose - /// CurrentlyEquippedLocation is non-None. - /// - [Fact] - public void SampleTable_hasEquippedItems() - { - var t = SampleData.BuildObjectTable(); - - bool anyEquipped = false; - foreach (var obj in t.Objects) - { - if (obj.CurrentlyEquippedLocation != EquipMask.None) - { anyEquipped = true; break; } - } - - Assert.True(anyEquipped, "Expected at least one equipped item in sample table"); - } - - /// - /// Sample table must include at least 2 side-bag containers in the - /// player's pack (ContainerId == PlayerGuid, Type has Container bit). - /// - [Fact] - public void SampleTable_hasSideBags() - { - var t = SampleData.BuildObjectTable(); - - int bagCount = 0; - foreach (var guid in t.GetContents(SampleData.PlayerGuid)) - { - var obj = t.Get(guid); - if (obj is not null && (obj.Type & ItemType.Container) != 0) - bagCount++; - } - - Assert.True(bagCount >= 2, - $"Expected >= 2 side-bag containers in player pack; got {bagCount}"); - } - - /// - /// Side bags must match the InventoryController filter: ItemType.Container OR ItemsCapacity > 0. - /// This ensures they appear in the side-bag column even if the exact Type flag changes. - /// - [Fact] - public void SampleTable_sideBags_matchInventoryControllerFilter() - { - var t = SampleData.BuildObjectTable(); - - int bagCount = 0; - foreach (var guid in t.GetContents(SampleData.PlayerGuid)) - { - var obj = t.Get(guid); - if (obj is null) continue; - // InventoryController.Populate line ~203: Type.HasFlag(Container) OR ItemsCapacity > 0 - bool isBag = obj.Type.HasFlag(ItemType.Container) || obj.ItemsCapacity > 0; - if (isBag) bagCount++; - } - - Assert.True(bagCount >= 2, - $"Expected >= 2 items matching the side-bag filter; got {bagCount}"); - } - - /// - /// Equipped items must BOTH (a) retain CurrentlyEquippedLocation != None after - /// seeding AND (b) appear in GetContents(PlayerGuid) so PaperdollController.Populate - /// can find them. These are the two conditions PaperdollController checks on every - /// equipped item (PaperdollController.Populate: ContainerId == playerGuid AND - /// CurrentlyEquippedLocation != None). - /// - [Fact] - public void SampleTable_equippedItems_retainLocationAndAreInContents() - { - var t = SampleData.BuildObjectTable(); - var contents = new System.Collections.Generic.HashSet(t.GetContents(SampleData.PlayerGuid)); - - int equippedCount = 0; - foreach (var obj in t.Objects) - { - if (obj.CurrentlyEquippedLocation == EquipMask.None) continue; - equippedCount++; - - // Must appear in GetContents so the controller can pick them up. - Assert.True(contents.Contains(obj.ObjectId), - $"Equipped item 0x{obj.ObjectId:X8} ('{obj.Name}', loc={obj.CurrentlyEquippedLocation}) " + - $"is NOT in GetContents(PlayerGuid). PaperdollController will miss it."); - - // The equip location must survive the MoveItem call. - Assert.NotEqual(EquipMask.None, obj.CurrentlyEquippedLocation); - } - - Assert.True(equippedCount >= 3, - $"Expected >= 3 equipped items in sample table; got {equippedCount}"); - } -} diff --git a/tests/AcDream.App.Tests/Studio/LayoutSourceTests.cs b/tests/AcDream.App.Tests/Studio/LayoutSourceTests.cs deleted file mode 100644 index 5d478223..00000000 --- a/tests/AcDream.App.Tests/Studio/LayoutSourceTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -using AcDream.App.Studio; -using AcDream.App.UI; -using AcDream.App.UI.Layout; -using DatReaderWriter; -using DatReaderWriter.Options; - -namespace AcDream.App.Tests.Studio; - -/// -/// Unit tests for . The dat-backed test skips cleanly -/// when the real dats are not present (CI / dev machines without AC installed). -/// -public class LayoutSourceTests -{ - private static (uint handle, int width, int height) NoTex(uint _) => (1u, 1, 1); - - /// Resolve the client dat directory, or null if unavailable (skip the test). - private static string? ResolveDatDir() - { - var fromEnv = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR"); - if (!string.IsNullOrWhiteSpace(fromEnv) && Directory.Exists(fromEnv)) - return fromEnv; - var def = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - "Documents", "Asheron's Call"); - return Directory.Exists(def) ? def : null; - } - - /// - /// Load the vitals LayoutDesc (0x2100006C) from the real dats and verify - /// that LayoutSource returns a non-null root with the layout id findable. - /// Skips when the dats are unavailable. - /// - /// Note: the spec asserts FindElement(0x2100006Cu) — that is the layout dat - /// id, not an element id in the widget tree. The actual vitals root element id - /// is 0x100005F9 (confirmed from vitals_2100006C.json fixture). We assert - /// FindElement(0x100005F9) here which verifies the same integration path: the - /// layout was successfully loaded and the element dict was populated. - /// - [Fact] - public void LoadsDatLayout_byId() - { - var dir = ResolveDatDir(); - if (dir is null) - return; // Skip: dats not available on this machine / CI. - - using var dats = new DatCollection(dir, DatAccessType.Read); - - var src = new LayoutSource(dats, NoTex, datFont: null); - var root = src.Load(new StudioOptions(dir, 0x2100006Cu, null)); - - // root must be non-null: Import found the LayoutDesc and built the tree. - Assert.NotNull(root); - Assert.NotNull(src.CurrentLayout); - // The vitals root element id is 0x100005F9 (layout dat id 0x2100006C ≠ element id). - // Asserting FindElement verifies the byId dict was populated by the importer. - Assert.NotNull(src.CurrentLayout!.FindElement(0x100005F9u)); - Assert.Equal(LayoutSourceKind.DatLayout, src.Kind); - } -} diff --git a/tests/AcDream.App.Tests/Studio/StudioWindowTests.cs b/tests/AcDream.App.Tests/Studio/StudioWindowTests.cs deleted file mode 100644 index 9f10682b..00000000 --- a/tests/AcDream.App.Tests/Studio/StudioWindowTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -using AcDream.App.Studio; -using AcDream.App.UI; - -namespace AcDream.App.Tests.Studio; - -public class StudioWindowTests -{ - [Fact] - public void FrameCloseGate_defersCloseUntilFrameCompletion() - { - var gate = new StudioFrameCloseGate(); - int closeCount = 0; - - gate.Request(); - - Assert.True(gate.IsRequested); - Assert.Equal(0, closeCount); - - gate.CompleteFrame(() => closeCount++); - gate.CompleteFrame(() => closeCount++); - - Assert.False(gate.IsRequested); - Assert.Equal(1, closeCount); - } - - [Fact] - public void ParseMockup_doesNotDefaultToSinglePanelLayout() - { - var opts = StudioOptions.Parse(new[] { @"C:\fake-dats", "--mockup" }); - - Assert.True(opts.Mockup); - Assert.Null(opts.LayoutId); - Assert.Null(opts.DumpSlug); - Assert.Null(opts.MarkupPath); - } - - [Fact] - public void ParseCapabilitySmokeOptions() - { - var opts = StudioOptions.Parse( - [ - @"C:\fake-dats", - "--mockup", - "--capability-report", - "report.json", - "--audio-smoke", - ]); - - Assert.Equal("report.json", opts.CapabilityReportPath); - Assert.True(opts.AudioSmoke); - } - - [Fact] - public void NormalizeSinglePanelRoot_movesDatPanelToCanvasOrigin() - { - var root = new UiPanel - { - Left = 500f, - Top = 138f, - Width = 300f, - Height = 362f, - Anchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right, - }; - - StudioWindow.NormalizeSinglePanelRoot(root); - - Assert.Equal(0f, root.Left); - Assert.Equal(0f, root.Top); - Assert.Equal(300f, root.Width); - Assert.Equal(362f, root.Height); - Assert.Equal(AnchorEdges.None, root.Anchors); - } -} diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterControllerTests.cs index be4e4d60..e2037542 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Studio; using AcDream.App.UI; using AcDream.App.UI.Layout; diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterLayoutImportProbe.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterLayoutImportProbe.cs index d2b334fb..712d516f 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterLayoutImportProbe.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterLayoutImportProbe.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using AcDream.App.Studio; using AcDream.App.UI; using AcDream.App.UI.Layout; using DatReaderWriter; diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs index 5d2a39f0..16e3de9c 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Studio; using AcDream.App.UI; using AcDream.App.UI.Layout; using System.Numerics; diff --git a/tests/AcDream.App.Tests/World/RuntimeEntityOwnershipTests.cs b/tests/AcDream.App.Tests/World/RuntimeEntityOwnershipTests.cs index 38e9d976..4d9bdf28 100644 --- a/tests/AcDream.App.Tests/World/RuntimeEntityOwnershipTests.cs +++ b/tests/AcDream.App.Tests/World/RuntimeEntityOwnershipTests.cs @@ -296,8 +296,7 @@ public sealed class RuntimeEntityOwnershipTests string? ns = type.Namespace; return ns?.StartsWith("AcDream.App", StringComparison.Ordinal) == true || ns?.StartsWith("AcDream.UI", StringComparison.Ordinal) == true - || ns?.StartsWith("Silk.NET", StringComparison.Ordinal) == true - || ns?.StartsWith("ImGuiNET", StringComparison.Ordinal) == true; + || ns?.StartsWith("Silk.NET", StringComparison.Ordinal) == true; } private static bool IsGuidDictionary(FieldInfo field) => diff --git a/tests/AcDream.Runtime.Tests/Entities/RuntimeEntityDirectoryTests.cs b/tests/AcDream.Runtime.Tests/Entities/RuntimeEntityDirectoryTests.cs index 19b82656..565800e8 100644 --- a/tests/AcDream.Runtime.Tests/Entities/RuntimeEntityDirectoryTests.cs +++ b/tests/AcDream.Runtime.Tests/Entities/RuntimeEntityDirectoryTests.cs @@ -151,8 +151,7 @@ public sealed class RuntimeEntityDirectoryTests Assert.DoesNotContain(exposed, type => type.Namespace?.StartsWith("AcDream.App", StringComparison.Ordinal) == true - || type.Namespace?.StartsWith("Silk.NET", StringComparison.Ordinal) == true - || type.Namespace?.StartsWith("ImGuiNET", StringComparison.Ordinal) == true); + || type.Namespace?.StartsWith("Silk.NET", StringComparison.Ordinal) == true); } [Fact]