acdream/src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs
Erik 1b4f3bac6b feat(B.6 slice 1): DebugPanel mirror for ProbeAutoWalk checkbox
Wires PhysicsDiagnostics.ProbeAutoWalkEnabled into the DebugVM + ImGui
panel checkbox alongside the existing Probe Resolve / Probe Cell /
Probe BSP hits checkboxes. Following the L.2a + L.2d pattern: the
panel toggle takes effect live (no relaunch needed) because the
diagnostic call sites read the static flag every frame.

Lets the next B.6 trace session toggle the probe via panel checkbox
when ACDREAM_DEVTOOLS=1, without an env-var dance.
2026-05-14 18:03:05 +02:00

282 lines
11 KiB
C#

using System.Numerics;
namespace AcDream.UI.Abstractions.Panels.Debug;
/// <summary>
/// The Phase I.2 debug panel — single ImGui window with collapsing-header
/// sections that replace the old custom <c>DebugOverlay</c>'s six floating
/// panels (Info / Stats / Help / Compass / Chat / Event) plus the toast
/// surface. Reads through <see cref="DebugVM"/> so values are always live.
///
/// <para>
/// Layout: Player Info, Performance, Compass, Help, Combat events, Recent
/// toasts, Diagnostics. Each section is a <c>CollapsingHeader</c>;
/// importance-ranked sections default open, niche ones default closed.
/// </para>
///
/// <para>
/// Reuses the I.1 widget extensions only; never imports a backend
/// namespace. Same constraints as <c>VitalsPanel</c> and <c>ChatPanel</c>.
/// </para>
/// </summary>
public sealed class DebugPanel : IPanel
{
private readonly DebugVM _vm;
public DebugPanel(DebugVM vm)
{
_vm = vm ?? throw new ArgumentNullException(nameof(vm));
}
/// <inheritdoc />
public string Id => "acdream.debug";
/// <inheritdoc />
public string Title => "Debug";
/// <inheritdoc />
public bool IsVisible { get; set; } = true;
/// <summary>
/// Cheat-sheet of currently meaningful keybinds. Kept as a static
/// table because the data is stable and the panel only renders
/// labels — no behavior change to the bindings themselves.
/// </summary>
private static readonly (string Key, string Action)[] Keybinds =
{
// K-fix4 (2026-04-26): refreshed for the retail-default keymap +
// Phase K input-pipeline bindings. F1-F12 alone are retail panel
// toggles; acdream debug actions live behind Ctrl+F* to avoid
// retail conflicts.
("Esc", "exit fly / close window"),
("F11", "open Settings (key rebinding etc.)"),
("Ctrl+Shift+F", "toggle free-fly camera"),
("Ctrl+F1", "toggle this debug panel"),
("Ctrl+F2", "toggle collision wireframes"),
("Ctrl+F3", "console dump (pos + nearby objects)"),
("Ctrl+F7", "cycle time-of-day override"),
("Ctrl+F8 / F9", "mouse sensitivity slower / faster"),
("Ctrl+F10", "cycle weather"),
("W / X", "run forward / backward"),
("A / D", "turn left / right"),
("Z / C", "strafe left / right"),
("Q", "autorun toggle"),
("Shift", "walk modifier (default = run)"),
("Space", "jump (hold to charge)"),
("Y G H B", "stand / sit / crouch / lie"),
("Hold MMB", "instant mouse-look"),
("Hold RMB", "free orbit camera around player"),
("Wheel", "zoom chase camera in / out"),
("Tab", "focus chat input"),
};
/// <inheritdoc />
public void Render(PanelContext ctx, IPanelRenderer renderer)
{
if (!renderer.Begin(Title))
{
renderer.End();
return;
}
DrawPlayerInfo(renderer);
DrawPerformance(renderer);
DrawCompass(renderer);
DrawHelp(renderer);
DrawCombatEvents(renderer);
DrawRecentToasts(renderer);
DrawDiagnostics(renderer);
renderer.End();
}
// ── Sections ──────────────────────────────────────────────────────
private void DrawPlayerInfo(IPanelRenderer r)
{
if (!r.CollapsingHeader("Player Info", defaultOpen: true)) return;
string mode = _vm.InPlayerMode ? "PLAYER"
: _vm.InFlyMode ? "FLY"
: "ORBIT";
r.Text($"mode: {mode} cell: 0x{_vm.CellId:X8}");
var p = _vm.PlayerPosition;
r.Text($"pos: ({p.X,7:F1}, {p.Y,7:F1}, {p.Z,7:F2})");
r.Text($"heading: {_vm.HeadingDeg,3:F0}°");
r.Text($"grounded: {(_vm.OnGround ? "yes" : "no ")} vZ: {_vm.VerticalVelocity,5:F2}");
string near = float.IsPositiveInfinity(_vm.NearestObjDist)
? "---"
: $"{_vm.NearestObjDist,4:F1}m";
if (_vm.Colliding)
{
r.TextColored(new Vector4(1f, 0.4f, 0.35f, 1f),
$"near: {near} {_vm.NearestObjLabel} [BLOCKED]");
}
else
{
r.Text($"near: {near} {_vm.NearestObjLabel}");
}
if (_vm.InPlayerMode)
r.Text($"chase dist: {_vm.ChaseDistance,4:F1}m{(_vm.RmbOrbit ? " [RMB orbit]" : "")}");
r.Text($"sens: {_vm.MouseSensitivity:F3}x");
}
private void DrawPerformance(IPanelRenderer r)
{
if (!r.CollapsingHeader("Performance", defaultOpen: true)) return;
r.Text($"fps: {_vm.Fps,5:F0} frame: {_vm.FrameMs,5:F1} ms");
r.Text($"visible LB: {_vm.LandblocksVisible,3}/{_vm.LandblocksTotal,3} radius: {_vm.StreamingRadius}");
r.Text($"entities: {_vm.EntityCount,4} animated: {_vm.AnimatedCount,3} coll: {_vm.ShadowObjectCount}");
r.Text($"lights: {_vm.ActiveLights}/{_vm.RegisteredLights} particles: {_vm.ParticleCount}");
r.Text($"time: {_vm.DayFraction,5:F2} {_vm.HourName} weather: {_vm.Weather}");
}
private void DrawCompass(IPanelRenderer r)
{
if (!r.CollapsingHeader("Compass", defaultOpen: false)) return;
// Phase I.2 stub — the visual strip + cardinal markers from the
// old DebugOverlay relied on raw 2D-rect primitives we don't (and
// shouldn't) expose through IPanelRenderer. The fancy compass
// strip lands in D.6 with proper world-HUD draw-list primitives.
// For now show heading degrees + compass cardinal label.
float h = NormalizeDeg(_vm.HeadingDeg);
r.Text($"heading: {h,3:F0}° cardinal: {Cardinal(h)}");
}
private void DrawHelp(IPanelRenderer r)
{
if (!r.CollapsingHeader("Help", defaultOpen: false)) return;
r.BeginTable("debug.help", 2);
foreach (var (key, action) in Keybinds)
{
r.TableNextColumn();
r.Text(key);
r.TableNextColumn();
r.Text(action);
}
r.EndTable();
}
private void DrawCombatEvents(IPanelRenderer r)
{
if (!r.CollapsingHeader("Combat events", defaultOpen: true)) return;
if (_vm.CombatEvents.Count == 0)
{
r.Text("(no recent combat)");
return;
}
foreach (var line in _vm.CombatEvents)
{
r.TextColored(ColorForCombat(line.Kind), line.Text);
}
}
private void DrawRecentToasts(IPanelRenderer r)
{
if (!r.CollapsingHeader("Recent toasts", defaultOpen: false)) return;
if (_vm.RecentToasts.Count == 0)
{
r.Text("(none)");
return;
}
foreach (var t in _vm.RecentToasts)
{
string ts = t.Timestamp.ToLocalTime().ToString("HH:mm:ss");
r.TextColored(ColorForToast(t.Kind), $"[{ts}] {t.Text}");
}
}
private void DrawDiagnostics(IPanelRenderer r)
{
if (!r.CollapsingHeader("Diagnostics", defaultOpen: true)) return;
bool dumpMotion = _vm.DumpMotion;
bool dumpVitals = _vm.DumpVitals;
bool dumpOpcodes = _vm.DumpOpcodes;
bool dumpSky = _vm.DumpSky;
bool probeResolve = _vm.ProbeResolve;
bool probeCell = _vm.ProbeCell;
bool probeBuilding = _vm.ProbeBuilding;
bool probeAutoWalk = _vm.ProbeAutoWalk;
if (r.Checkbox("Dump motion (ACDREAM_DUMP_MOTION)", ref dumpMotion)) _vm.DumpMotion = dumpMotion;
if (r.Checkbox("Dump vitals (ACDREAM_DUMP_VITALS)", ref dumpVitals)) _vm.DumpVitals = dumpVitals;
if (r.Checkbox("Dump opcodes (ACDREAM_DUMP_OPCODES)", ref dumpOpcodes)) _vm.DumpOpcodes = dumpOpcodes;
if (r.Checkbox("Dump sky (ACDREAM_DUMP_SKY)", ref dumpSky)) _vm.DumpSky = dumpSky;
// L.2a slice 1 (2026-05-12): unlike the four above, these
// forward to PhysicsDiagnostics so a toggle takes effect live.
if (r.Checkbox("Probe resolve (ACDREAM_PROBE_RESOLVE)", ref probeResolve)) _vm.ProbeResolve = probeResolve;
if (r.Checkbox("Probe cell-transit (ACDREAM_PROBE_CELL)",ref probeCell)) _vm.ProbeCell = probeCell;
// L.2d slice 1 (2026-05-13): heavy per-hit BSP diagnostic for
// doorway / building shape-fidelity work. Emits multi-line
// [resolve-bldg] entries; expect log volume to spike at walls.
if (r.Checkbox("Probe BSP hits (ACDREAM_PROBE_BUILDING, slow)",
ref probeBuilding)) _vm.ProbeBuilding = probeBuilding;
// B.6 slice 1 (2026-05-14): local-player auto-walk trace for issue #63.
// Low volume — only the local player's UM/UP/Use/PickUp events emit.
if (r.Checkbox("Probe auto-walk (ACDREAM_PROBE_AUTOWALK)",
ref probeAutoWalk)) _vm.ProbeAutoWalk = probeAutoWalk;
r.Spacing();
// Cycle / toggle actions live on the VM as Action handles; the
// host (GameWindow) populates them with the same lambdas the
// old F7/F10/F2 keybinds used.
if (r.Button("Cycle time of day")) _vm.CycleTimeOfDay?.Invoke();
r.SameLine();
if (r.Button("Cycle weather")) _vm.CycleWeather?.Invoke();
r.SameLine();
if (r.Button("Toggle collision wires")) _vm.ToggleCollisionWires?.Invoke();
// Phase K.2 — explicit free-fly toggle button. Mirrors the
// legacy F-key alias but is discoverable to users who haven't
// memorized the Ctrl+F* debug bindings. Action handle owned
// by GameWindow; null-safe for tests / offline.
if (r.Button("Toggle Free-Fly Mode")) _vm.ToggleFlyMode?.Invoke();
r.Text(_vm.DebugWireframes ? "collision wires: ON" : "collision wires: OFF");
}
// ── Color helpers ─────────────────────────────────────────────────
private static Vector4 ColorForCombat(CombatEventKind kind) => kind switch
{
CombatEventKind.Info => new Vector4(1.0f, 0.9f, 0.3f, 1f), // yellow
CombatEventKind.Warn => new Vector4(1.0f, 0.5f, 0.5f, 1f), // light red
CombatEventKind.Error => new Vector4(1.0f, 0.3f, 0.3f, 1f), // deep red
_ => new Vector4(1f, 1f, 1f, 1f),
};
private static Vector4 ColorForToast(ToastKind kind) => kind switch
{
ToastKind.Warn => new Vector4(1.0f, 0.8f, 0.4f, 1f),
ToastKind.Error => new Vector4(1.0f, 0.4f, 0.4f, 1f),
_ => new Vector4(0.85f, 0.95f, 1.0f, 1f),
};
private static float NormalizeDeg(float deg)
{
deg %= 360f;
if (deg < 0) deg += 360f;
return deg;
}
private static string Cardinal(float deg)
{
// Heading 0 = +X (east) per the old overlay. Same eight cardinal
// labels — N/E/S/W with NE/SE/SW/NW between.
// 0=E, 90=N, 180=W, 270=S (acdream's coordinate convention).
string[] dirs = { "E", "NE", "N", "NW", "W", "SW", "S", "SE" };
int idx = (int)MathF.Round(deg / 45f) & 7;
return dirs[idx];
}
}