feat(render): Campaign V slice V11 commit 1 - delete ImGui, Studio, and the DevTools frontend

The ImGui developer-tools stack (AcDream.UI.ImGui), UI Studio
(src/AcDream.App/Studio), and the DevToolsFramePresenter/
SettingsDevToolsCompositionPhase ImGui composition machinery are removed.
Vulkan never composed a DevTools frontend (DevToolsEnabled already forced
false whenever the backend was Vulkan); this commit makes that permanent by
deleting the only implementation rather than leaving a dead branch behind.

What moved: Studio/SampleData.cs is a live production dependency
(InteractionRetainedUiComposition's character-sheet fallback, plus three
UI.Layout test files) - git mv'd to src/AcDream.App/UI/Layout/SampleData.cs,
namespace AcDream.App.UI.Layout, and trimmed to the SampleCharacter API that
is actually still called (BuildObjectTable/AddItem/AddEquipped/the item-guid
and icon constants had zero callers left once the Studio fixture provider
that used them was deleted).

What survives as backend-neutral seams, per the tests that still exercise
them: IDevToolsFrameLifecycle (moved into RenderFramePreparationController.cs,
now always bound to null), IFramebufferDevToolsTarget/FramebufferDevToolsBinding
in FramebufferResizeController.cs (its concrete DevToolsFramebufferTarget
adapter is deleted), and IDevToolsGameplayCommands in
GameplayInputCommandController.cs (DevToolsGameplayCommands becomes a
documented no-op instead of forwarding to the deleted presenter). A follow-up
re-homes Settings/Debug onto the retained UI through IPanelRenderer; until
then keybind remapping falls back to editing keybinds.json.

DevToolsEnabled is now `private const bool DevToolsEnabled = false`.
RuntimeOptions.DevTools is unchanged and still reaches VulkanGraphicsContext
for the optional debug-utils extensions; Program.cs now logs one line when
ACDREAM_DEVTOOLS=1 explaining that the ImGui UI is gone and the flag is
Vulkan-only now.

Removed: AcDream.UI.ImGui (project + ImGui.NET/Silk.NET.OpenGL.Extensions.ImGui
package refs), src/AcDream.App/Studio (minus SampleData.cs),
DevToolsFramePresenter.cs and everything only it constructed
(ISettingsDevToolsCompositionFactory, RetailSettingsDevToolsCompositionFactory,
DevToolsCompositionOwner, IGameWindowSettingsDevToolsPublication,
SettingsDevToolsOptionalDependencies, the "developer tools" shutdown-ledger
stage and its DevTools-typed fields on IngressShutdownRoots/
RenderShutdownRoots), the ui-studio Program.cs verb, and the cimgui native
manifest entries in GraphicalHostPlatformServices. GameWindow.cs's DevTools
composition branch, its _vitalsVm/_debugVm/_devToolsComposition/
_devToolsFramePresenter/_devToolsCommandBus fields, and every settingsDevTools
.DevTools?.* access across FrameRootComposition.cs/SessionPlayerComposition.cs
are gone with it.

Build green; complete Release solution suite 8,830 / 5 skips (App Tests
4,097/3 skips run standalone - one #250-family zero-allocation test flakes
under the full parallel `dotnet test AcDream.slnx` run, a pre-existing,
documented class unrelated to this change).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-28 23:56:04 +02:00
parent db4426d5ef
commit 844cf092a1
48 changed files with 227 additions and 6201 deletions

View file

@ -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(

View file

@ -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(

View file

@ -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<string>? 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<string>? 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,

View file

@ -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;
/// <summary>
/// Resolved display/audio/quality settings for the session. The optional
/// ImGui developer-tools frontend that used to compose here (VitalsPanel,
/// ChatPanel, DebugPanel, SettingsPanel via <c>AcDream.UI.ImGui</c>) 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
/// <c>IPanelRenderer</c> implementation; until then keybind remapping falls
/// back to editing keybinds.json.
/// </summary>
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<string> 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);
/// <summary>
/// Complete optional developer frontend lifetime. Construction rollback and
/// normal shutdown share the same named, retryable release operations.
/// </summary>
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;
}
}
/// <summary>
/// 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.
/// </summary>
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<SettingsDevToolsCompositionPoint>? _faultInjection;
public SettingsDevToolsCompositionPhase(
SettingsDevToolsDependencies dependencies,
IGameWindowSettingsDevToolsPublication publication,
ISettingsDevToolsCompositionFactory? factory = null,
Action<SettingsDevToolsCompositionPoint>? 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<GameWindowGraphics, IInputContext> 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<RuntimeSettingsViewModelBinding>?
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);
}