Make every App composition phase borrow one GameRuntime, retire the duplicate view/event adapters, and dispose the root only after its graphical borrowers release. This preserves synchronous UI commands while giving shutdown one exact ownership ledger. Co-authored-by: OpenAI Codex <codex@openai.com>
644 lines
23 KiB
C#
644 lines
23 KiB
C#
using AcDream.App.Combat;
|
|
using AcDream.App.Diagnostics;
|
|
using AcDream.App.Input;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Settings;
|
|
using AcDream.Core.Chat;
|
|
using AcDream.Core.Combat;
|
|
using AcDream.Core.Player;
|
|
using AcDream.Runtime;
|
|
using AcDream.Runtime.Gameplay;
|
|
using AcDream.UI.Abstractions.Input;
|
|
using AcDream.UI.Abstractions.Panels.Chat;
|
|
using AcDream.UI.Abstractions.Panels.Debug;
|
|
using AcDream.UI.Abstractions.Panels.Settings;
|
|
using AcDream.UI.Abstractions.Panels.Vitals;
|
|
using AcDream.UI.ImGui;
|
|
using Silk.NET.Input;
|
|
using Silk.NET.OpenGL;
|
|
using Silk.NET.Windowing;
|
|
|
|
namespace AcDream.App.Composition;
|
|
|
|
internal sealed record SettingsDevToolsResult(
|
|
AcDream.UI.Abstractions.Settings.QualitySettings ResolvedQuality,
|
|
DevToolsCompositionOwner? DevTools);
|
|
|
|
internal sealed record SettingsDevToolsOptionalDependencies(
|
|
IDevToolsRuntimeFacts Facts,
|
|
IRuntimeKeyBindingTarget? KeyBindingTarget,
|
|
DeferredCanonicalWorldEntityCountSource WorldEntities,
|
|
DeferredRenderFrameDiagnosticsSource FrameDiagnostics,
|
|
DeferredDevToolsPlayerModeCommands PlayerModeCommands);
|
|
|
|
internal sealed record SettingsDevToolsDependencies(
|
|
IView Window,
|
|
RuntimeSettingsController Settings,
|
|
IRuntimeSettingsStartupTarget StartupTarget,
|
|
HostQuiescenceGate HostQuiescence,
|
|
GameRuntime Runtime,
|
|
SettingsDevToolsOptionalDependencies? DevTools,
|
|
RuntimeDiagnosticCommandSlot DiagnosticCommands,
|
|
CombatFeedbackSlot CombatFeedback,
|
|
KeyBindings KeyBindings,
|
|
FrameProfiler FrameProfiler,
|
|
FramebufferResizeController FramebufferResize,
|
|
Action<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,
|
|
}
|
|
|
|
/// <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.
|
|
/// </summary>
|
|
internal sealed class SettingsDevToolsCompositionPhase :
|
|
ISettingsDevToolsCompositionPhase<
|
|
GameWindowPlatformResult<GL, IInputContext>,
|
|
HostInputCameraResult,
|
|
ContentEffectsAudioResult,
|
|
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)
|
|
{
|
|
_dependencies = dependencies ?? throw new ArgumentNullException(nameof(dependencies));
|
|
_publication = publication ?? throw new ArgumentNullException(nameof(publication));
|
|
_factory = factory ?? new RetailSettingsDevToolsCompositionFactory();
|
|
_faultInjection = faultInjection;
|
|
}
|
|
|
|
public SettingsDevToolsResult Compose(
|
|
GameWindowPlatformResult<GL, IInputContext> platform,
|
|
HostInputCameraResult host,
|
|
ContentEffectsAudioResult content)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(platform);
|
|
ArgumentNullException.ThrowIfNull(host);
|
|
ArgumentNullException.ThrowIfNull(content);
|
|
|
|
_dependencies.Settings.ApplyStartup(_dependencies.StartupTarget);
|
|
Fault(SettingsDevToolsCompositionPoint.SettingsApplied);
|
|
|
|
DevToolsCompositionOwner? devTools = _dependencies.DevTools is { } optional
|
|
? ComposeOptionalDevTools(platform, host, optional)
|
|
: null;
|
|
return new SettingsDevToolsResult(
|
|
_dependencies.Settings.ResolvedQuality,
|
|
devTools);
|
|
}
|
|
|
|
private DevToolsCompositionOwner? ComposeOptionalDevTools(
|
|
GameWindowPlatformResult<GL, 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",
|
|
() => _factory.CreateBootstrap(
|
|
platform.Graphics,
|
|
_dependencies.Window,
|
|
input),
|
|
static value => value.Dispose());
|
|
IImGuiBootstrapper bootstrap = bootstrapLease.Resource;
|
|
Fault(SettingsDevToolsCompositionPoint.BootstrapCreated);
|
|
|
|
input.Activate();
|
|
Fault(SettingsDevToolsCompositionPoint.InputActivated);
|
|
|
|
ImGuiPanelHost panelHost = _factory.CreatePanelHost();
|
|
Fault(SettingsDevToolsCompositionPoint.PanelHostCreated);
|
|
|
|
var vitals = new VitalsVM(
|
|
_dependencies.Combat,
|
|
_dependencies.LocalPlayer);
|
|
var vitalsPanel = new VitalsPanel(vitals);
|
|
panelHost.Register(vitalsPanel);
|
|
Fault(SettingsDevToolsCompositionPoint.VitalsRegistered);
|
|
|
|
var chatLease = scope.Acquire(
|
|
"developer chat view model",
|
|
() => new ChatVM(
|
|
_dependencies.Communication.Chat,
|
|
commandTargets:
|
|
_dependencies.Communication.CommandTargets)
|
|
{
|
|
FpsProvider = () => optional.Facts.Fps,
|
|
PositionProvider = () => optional.Facts.PlayerPosition,
|
|
},
|
|
static value => value.Dispose());
|
|
ChatVM chatVm = chatLease.Resource;
|
|
var chatPanel = new ChatPanel(chatVm);
|
|
panelHost.Register(chatPanel);
|
|
Fault(SettingsDevToolsCompositionPoint.ChatRegistered);
|
|
|
|
var debugLease = scope.Acquire(
|
|
"developer debug view model",
|
|
() => CreateDebugViewModel(optional.Facts),
|
|
static value => value.Dispose());
|
|
DebugVM debugVm = debugLease.Resource;
|
|
Fault(SettingsDevToolsCompositionPoint.DebugViewModelCreated);
|
|
debugVm.CycleTimeOfDay = _dependencies.DiagnosticCommands.CycleTimeOfDay;
|
|
debugVm.CycleWeather = _dependencies.DiagnosticCommands.CycleWeather;
|
|
debugVm.ToggleCollisionWires =
|
|
_dependencies.DiagnosticCommands.ToggleCollisionWireframes;
|
|
debugVm.ToggleFlyMode = optional.PlayerModeCommands.ToggleFlyOrChase;
|
|
Fault(SettingsDevToolsCompositionPoint.DiagnosticActionsBound);
|
|
|
|
var combatBindingLease = scope.Acquire(
|
|
"combat feedback binding",
|
|
() => new CombatFeedbackBinding(
|
|
_dependencies.CombatFeedback,
|
|
debugVm),
|
|
static value => value.Dispose());
|
|
Fault(SettingsDevToolsCompositionPoint.CombatFeedbackBound);
|
|
|
|
var debugPanel = new DebugPanel(debugVm);
|
|
panelHost.Register(debugPanel);
|
|
Fault(SettingsDevToolsCompositionPoint.DebugRegistered);
|
|
|
|
RuntimeSettingsViewModelBinding? settingsBinding = null;
|
|
SettingsPanel? settingsPanel = null;
|
|
CompositionAcquisitionScope.CompositionAcquisitionLease<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);
|
|
}
|