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]