Move streaming, live-session, hydration, local-player, combat, and teleport construction behind the typed Phase-7 boundary. Add exact-owner runtime bindings and focused spawn-claim classification so partial startup rolls back without retaining old session targets while preserving the accepted construction and frame dependencies. Co-authored-by: Codex <codex@openai.com>
491 lines
15 KiB
C#
491 lines
15 KiB
C#
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);
|
|
}
|
|
|
|
/// <summary>Concrete ImGui backend for the developer presentation owner.</summary>
|
|
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;
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>Mutable late-composition seam for player-mode menu operations.</summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>Resolves the reconnect-safe command bus at draw time.</summary>
|
|
internal sealed class DevToolsCommandBusSource : IDevToolsCommandBusSource
|
|
{
|
|
private LiveSessionController? _session;
|
|
private bool _deactivated;
|
|
|
|
public ICommandBus Current =>
|
|
!_deactivated && _session is { } session
|
|
? session.Commands
|
|
: NullCommandBus.Instance;
|
|
|
|
public void Bind(LiveSessionController 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(LiveSessionController 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(LiveSessionController 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 LiveSessionController _expected;
|
|
|
|
public Binding(
|
|
DevToolsCommandBusSource owner,
|
|
LiveSessionController expected)
|
|
{
|
|
_owner = owner;
|
|
_expected = expected;
|
|
}
|
|
|
|
public void Dispose() =>
|
|
Interlocked.Exchange(ref _owner, null)?.Unbind(_expected);
|
|
}
|
|
}
|
|
|
|
/// <summary>Typed panel operations used by menu and input presentation.</summary>
|
|
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)),
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Owns the optional ImGui developer frame, menu policy, panel actions, and
|
|
/// reusable default layout. Retained gameplay UI remains a separate earlier
|
|
/// presentation phase.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|