Adds the [indoor-bsp] probe + ProbeIndoorBspEnabled toggle for the Indoor walking Phase 1 BSP-cluster investigation. Mirrors the existing [resolve] / [cell-transit] / [indoor-*] pattern: one log line per BSPQuery.FindCollisions call from FindEnvCollisions' cell branch, capturing cell id, sphere local-pos, result TransitionState, and the hit poly's normal + side-type via the LastBspHitPoly side-channel (already wired for ProbeBuildingEnabled, now also fires for the indoor flag). Toggle via ACDREAM_PROBE_INDOOR_BSP=1 env var or DebugPanel checkbox. Zero-cost when off. Predecessor for the three fix commits that will close ISSUES.md #84/#85/#86 after the capture session. Spec: docs/superpowers/specs/2026-05-19-indoor-walking-phase1-bsp-cluster-design.md Plan: docs/superpowers/plans/2026-05-19-indoor-walking-phase1-bsp-cluster.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
334 lines
14 KiB
C#
334 lines
14 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);
|
|
DrawChaseCamera(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 DrawChaseCamera(IPanelRenderer r)
|
|
{
|
|
if (!r.CollapsingHeader("Chase camera", defaultOpen: true)) return;
|
|
|
|
bool useRetail = _vm.UseRetailChaseCamera;
|
|
bool alignSlope = _vm.CameraAlignToSlope;
|
|
float tStiff = _vm.CameraTranslationStiffness;
|
|
float rStiff = _vm.CameraRotationStiffness;
|
|
float lpWindow = _vm.CameraMouseLowPassWindowSec;
|
|
float adjSpeed = _vm.CameraAdjustmentSpeed;
|
|
|
|
if (r.Checkbox("Use retail chase camera (env: ACDREAM_RETAIL_CHASE)", ref useRetail))
|
|
_vm.UseRetailChaseCamera = useRetail;
|
|
|
|
if (r.Checkbox("Align to slope (env: ACDREAM_CAMERA_ALIGN_SLOPE)", ref alignSlope))
|
|
_vm.CameraAlignToSlope = alignSlope;
|
|
|
|
if (r.SliderFloat("Translation stiffness", ref tStiff, 0.05f, 1.0f))
|
|
_vm.CameraTranslationStiffness = tStiff;
|
|
if (r.SliderFloat("Rotation stiffness", ref rStiff, 0.05f, 1.0f))
|
|
_vm.CameraRotationStiffness = rStiff;
|
|
if (r.SliderFloat("Mouse low-pass window (s)", ref lpWindow, 0.0f, 0.5f))
|
|
_vm.CameraMouseLowPassWindowSec = lpWindow;
|
|
if (r.SliderFloat("Adjustment speed (units/s)", ref adjSpeed, 10f, 80f))
|
|
_vm.CameraAdjustmentSpeed = adjSpeed;
|
|
}
|
|
|
|
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;
|
|
|
|
// ── Indoor rendering diagnostics (2026-05-19) ───────────────
|
|
// Pinpoint where the EnvCell rendering chain breaks for
|
|
// hypothesis-driven Phase 2 fix. Spec:
|
|
// docs/superpowers/specs/2026-05-19-indoor-cell-rendering-fix-design.md
|
|
r.Separator();
|
|
r.Text("Indoor rendering (envCell):");
|
|
|
|
bool probeIndoorAll = _vm.ProbeIndoorAll;
|
|
bool probeIndoorWalk = _vm.ProbeIndoorWalk;
|
|
bool probeIndoorLookup = _vm.ProbeIndoorLookup;
|
|
bool probeIndoorUpload = _vm.ProbeIndoorUpload;
|
|
bool probeIndoorXform = _vm.ProbeIndoorXform;
|
|
bool probeIndoorCull = _vm.ProbeIndoorCull;
|
|
|
|
if (r.Checkbox("Indoor: ALL (ACDREAM_PROBE_INDOOR_ALL)", ref probeIndoorAll)) _vm.ProbeIndoorAll = probeIndoorAll;
|
|
if (r.Checkbox("Indoor: walk (ACDREAM_PROBE_INDOOR_WALK)", ref probeIndoorWalk)) _vm.ProbeIndoorWalk = probeIndoorWalk;
|
|
if (r.Checkbox("Indoor: lookup (ACDREAM_PROBE_INDOOR_LOOKUP)", ref probeIndoorLookup)) _vm.ProbeIndoorLookup = probeIndoorLookup;
|
|
if (r.Checkbox("Indoor: upload (ACDREAM_PROBE_INDOOR_UPLOAD)", ref probeIndoorUpload)) _vm.ProbeIndoorUpload = probeIndoorUpload;
|
|
if (r.Checkbox("Indoor: xform (ACDREAM_PROBE_INDOOR_XFORM)", ref probeIndoorXform)) _vm.ProbeIndoorXform = probeIndoorXform;
|
|
if (r.Checkbox("Indoor: cull (ACDREAM_PROBE_INDOOR_CULL)", ref probeIndoorCull)) _vm.ProbeIndoorCull = probeIndoorCull;
|
|
|
|
bool probeIndoorBsp = _vm.ProbeIndoorBsp;
|
|
if (r.Checkbox("Indoor: BSP collision (ACDREAM_PROBE_INDOOR_BSP)", ref probeIndoorBsp)) _vm.ProbeIndoorBsp = probeIndoorBsp;
|
|
|
|
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];
|
|
}
|
|
}
|