From 05bfe8d162db22722d840a8a66d768628d5eebd1 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 24 Aug 2026 10:58:59 +0200 Subject: [PATCH] fix #434: delete the unreachable DebugPanel/DebugVM surface and the comments that advertised it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DebugPanel and DebugVM have not been constructible since Campaign V slice V11 removed the ImGui frontend that hosted them: nothing in src/ ever called their constructors, only a test did. Two consequences, both fixed here — 35 environment reads inside them were unreachable, and roughly forty XML doc comments across the diagnostics owners promised a runtime checkbox that no longer exists. A flag documented as runtime-toggleable when it is startup-only sends the next investigation down a path that cannot work. Deleted DebugPanel.cs (340 lines), DebugVM.cs (548) and DebugVMTests.cs (327). Corrected the surviving claims in PhysicsDiagnostics, RenderingDiagnostics, CameraDiagnostics, PhysicsEngine and GameWindow to say what is actually true: these flags are set from the environment at startup or by direct assignment. The one real dependant was CombatFeedbackSlot, whose binding target was DebugVM. It now takes a plain Action, which removes the dependency without changing behavior — and makes visible that there is no behavior: nothing binds the slot, so the combat refusals it carries ("No monster target", "Enter melee or missile combat first") have been discarded all along. Filed as #436 and pinned by a test, rather than papered over with an invented chat message; the retail text and channel need the oracle first. Deliberately untouched: F1's AcdreamToggleDebugPanel binding, which GameplayInputCommandController consumes as a documented no-op so the key does not fall through to a lower input scope; and the DebugVmRenderFactsPublisher / DevToolsRuntimeSources chain, which is still wired into production composition and deserves its own dead-code pass instead of being pulled into this one. Full hermetic suite 15,333 passed / 0 failed. Co-Authored-By: Claude Fable 5 --- docs/ISSUES.md | 46 +- .../Combat/LiveCombatAttackOperations.cs | 34 +- src/AcDream.App/Rendering/GameWindow.cs | 4 +- .../Physics/PhysicsDiagnostics.cs | 26 +- src/AcDream.Core/Physics/PhysicsEngine.cs | 2 +- .../Rendering/CameraDiagnostics.cs | 5 +- .../Rendering/RenderingDiagnostics.cs | 18 +- .../Panels/Debug/DebugPanel.cs | 340 ----------- .../Panels/Debug/DebugVM.cs | 548 ------------------ .../Combat/CombatFeedbackSlotTests.cs | 73 +-- .../Panels/Debug/DebugVMTests.cs | 327 ----------- 11 files changed, 129 insertions(+), 1294 deletions(-) delete mode 100644 src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs delete mode 100644 src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs delete mode 100644 tests/AcDream.UI.Abstractions.Tests/Panels/Debug/DebugVMTests.cs diff --git a/docs/ISSUES.md b/docs/ISSUES.md index d9866cfb..7af44839 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -76,9 +76,53 @@ id list), so deletion order matters. --- -## #434 — The DebugPanel/DebugVM developer surface is unreachable, and ~40 doc comments still advertise it as live +## #436 — Combat refusal text ("No monster target") is silently dropped **Status:** OPEN +**Severity:** MEDIUM (missing user feedback on a common action) +**Filed:** 2026-08-24 (exposed by #434's dead-code removal) +**Component:** combat / chat presentation + +**Symptom:** `LiveCombatAttackOperations` produces two refusal messages — +`"Enter melee or missile combat first"` and `"No monster target"` — and +hands them to `CombatFeedbackSlot.Show`. Nothing in `src/` ever binds a +target to that slot, so **both messages go nowhere**. Attacking with no +target, or without a combat mode, gives the player no explanation at all. + +**How it got here:** the slot's binding target used to be the developer +`DebugVM`, which Campaign V slice V11 left unreachable (see #434). Nothing +noticed because the drop is silent — `Show` is a null-conditional invoke. +#434 converted the seam to a plain `Action` so it no longer depends +on deleted code, and pinned the current drop-on-the-floor behavior in +`CombatFeedbackSlotTests.AnUnboundSlotDropsItsMessages` so the day it gets a +real binder, that test is what changes. + +**Fix shape:** route the slot to the chat window, where retail puts this +text. Needs the retail oracle first: confirm the exact strings and their +LogTextType/channel (`claude-memory/project_chat_digest.md` has the color +and channel map) rather than inventing wording — retail's own text may +differ from these two placeholder strings. + +--- + +## #434 — CLOSED: The DebugPanel/DebugVM developer surface is unreachable, and ~40 doc comments still advertise it as live + +**Status:** CLOSED 2026-08-24. Deleted `DebugPanel.cs` (340 lines), +`DebugVM.cs` (548) and `DebugVMTests.cs` (327) — 1,215 lines. Converted the +one real dependant (`CombatFeedbackSlot`) to a delegate seam, which exposed +#436. Corrected every false "runtime-toggleable via the DebugPanel" claim in +`PhysicsDiagnostics`, `RenderingDiagnostics`, `CameraDiagnostics`, +`PhysicsEngine` and `GameWindow`. Full hermetic suite 15,333 passed / 0 +failed. **Deliberately left alone:** F1's `AcdreamToggleDebugPanel` binding, +which `GameplayInputCommandController` consumes as a documented no-op on +purpose (so the key does not fall through to a lower scope); and the +`DebugVmRenderFactsPublisher` / `DevToolsRuntimeSources` chain, which is +still wired into production composition and needs its own dead-code pass +rather than being dragged into a documentation cleanup. + +**Original report follows.** + +**Status (original):** OPEN **Severity:** LOW (no runtime defect; a documentation-truth and dead-code problem) **Filed:** 2026-08-24 (found during the launch-options audit) **Component:** UI.Abstractions / diagnostics ownership diff --git a/src/AcDream.App/Combat/LiveCombatAttackOperations.cs b/src/AcDream.App/Combat/LiveCombatAttackOperations.cs index ccac0219..96816196 100644 --- a/src/AcDream.App/Combat/LiveCombatAttackOperations.cs +++ b/src/AcDream.App/Combat/LiveCombatAttackOperations.cs @@ -66,27 +66,39 @@ internal interface ICombatFeedbackSink void Show(string message); } +/// +/// Routes combat refusal text ("No monster target") to whichever surface is +/// bound to show it. +/// +/// +/// #434: the bound target used to be the developer DebugVM, which +/// Campaign V slice V11 left unreachable — nothing has constructed it since, +/// so both messages below have been going nowhere. The binding target is now a +/// plain delegate so this seam no longer depends on that dead class, but it +/// still has no production binder: wiring it to the chat window, where retail +/// puts this text, is #436. +/// internal sealed class CombatFeedbackSlot : ICombatFeedbackSink { - private AcDream.UI.Abstractions.Panels.Debug.DebugVM? _viewModel; + private Action? _target; - public void Bind(AcDream.UI.Abstractions.Panels.Debug.DebugVM viewModel) + public void Bind(Action target) { - ArgumentNullException.ThrowIfNull(viewModel); - if (_viewModel is not null && !ReferenceEquals(_viewModel, viewModel)) + ArgumentNullException.ThrowIfNull(target); + if (_target is not null && !ReferenceEquals(_target, target)) throw new InvalidOperationException( - "Combat feedback is already bound to a developer view model."); - _viewModel = viewModel; + "Combat feedback is already bound to a presentation target."); + _target = target; } - public void Unbind(AcDream.UI.Abstractions.Panels.Debug.DebugVM viewModel) + public void Unbind(Action target) { - ArgumentNullException.ThrowIfNull(viewModel); - if (ReferenceEquals(_viewModel, viewModel)) - _viewModel = null; + ArgumentNullException.ThrowIfNull(target); + if (ReferenceEquals(_target, target)) + _target = null; } - public void Show(string message) => _viewModel?.AddToast(message); + public void Show(string message) => _target?.Invoke(message); } internal sealed class CombatAttackOperationsSlot diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 93f8da2d..b0c4b52f 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -134,8 +134,8 @@ public sealed class GameWindow : private DebugLineRenderer? _debugLines; // K-fix4 (2026-04-26): default OFF. The orange BSP / green cylinder // wireframes are noisy outdoors and confuse first-time users into - // thinking they're a rendering bug. Ctrl+F2 toggles, the DebugPanel - // → Diagnostics → "Toggle collision wires" button toggles too. + // thinking they're a rendering bug. Ctrl+F2 toggles. (The DebugPanel + // button that also toggled it is gone — #434.) private readonly AcDream.App.Rendering.WorldSceneDebugState _worldSceneDebugState = new(); diff --git a/src/AcDream.Core/Physics/PhysicsDiagnostics.cs b/src/AcDream.Core/Physics/PhysicsDiagnostics.cs index 7af14853..57843b0b 100644 --- a/src/AcDream.Core/Physics/PhysicsDiagnostics.cs +++ b/src/AcDream.Core/Physics/PhysicsDiagnostics.cs @@ -6,10 +6,12 @@ namespace AcDream.Core.Physics; /// /// L.2a slice 1 (2026-05-12) — runtime-toggleable physics probe flags. -/// Initialized from env vars at process start; flippable at runtime via -/// the DebugPanel mirror (or by direct assignment). Log call sites read -/// these statics so a checkbox toggle takes effect on the next resolve -/// without relaunching. +/// Initialized from env vars at process start; flippable at runtime by +/// direct assignment. Log call sites read these statics so a change takes +/// effect on the next resolve without relaunching. (#434: these flags used +/// to have a DebugPanel checkbox mirror. That panel has been unreachable +/// since Campaign V slice V11 removed its ImGui host, so every flag here is +/// startup-or-assignment only.) /// /// /// L.2d slice 1 (2026-05-13) adds + @@ -157,7 +159,7 @@ public static class PhysicsDiagnostics /// /// /// Initial state from ACDREAM_PROBE_BUILDING=1. Mirrorable - /// via DebugVM.ProbeBuilding when ACDREAM_DEVTOOLS=1. + /// by direct assignment (its DebugVM mirror is gone — #434). /// /// /// @@ -891,7 +893,7 @@ public static class PhysicsDiagnostics /// /// /// Toggle via env var ACDREAM_PROBE_USEABILITY_FALLBACK=1 - /// or DebugPanel checkbox. + /// by direct assignment. /// public static bool ProbeUseabilityFallbackEnabled { get; set; } = Environment.GetEnvironmentVariable("ACDREAM_PROBE_USEABILITY_FALLBACK") == "1"; @@ -915,7 +917,7 @@ public static class PhysicsDiagnostics /// the post-collision velocity disagrees with retail. /// /// Initial state from ACDREAM_DUMP_STEEP_ROOF=1. Runtime-toggleable - /// via the property setter; not yet wired to a DebugPanel checkbox (open + /// via the property setter (open /// follow-up if a debugging session calls for it). /// public static bool DumpSteepRoofEnabled { get; set; } = @@ -940,7 +942,7 @@ public static class PhysicsDiagnostics /// /// /// Initial state from ACDREAM_PROBE_INDOOR_BSP=1. - /// Runtime-toggleable via DebugPanel. + /// Runtime-toggleable by direct assignment. /// /// /// @@ -967,7 +969,7 @@ public static class PhysicsDiagnostics /// zero poly refs, candidate (b)/(d). /// /// This diagnostic fires at most once per EnvCell (cache is no-op after - /// first population). It does NOT have a DebugPanel mirror yet — this is + /// first population). This is /// a one-shot capture tool, not a persistent toggle. Promote to full /// infrastructure after the root cause is identified. /// @@ -1019,7 +1021,7 @@ public static class PhysicsDiagnostics /// /// /// Initial state from ACDREAM_PROBE_WALK_MISS=1. - /// No DebugPanel mirror — one-shot diagnostic. + /// One-shot diagnostic. /// /// /// @@ -1047,7 +1049,7 @@ public static class PhysicsDiagnostics /// /// /// Initial state from ACDREAM_PROBE_PUSH_BACK=1. - /// Runtime-toggleable via DebugVM mirror. + /// Runtime-toggleable by direct assignment. /// /// /// @@ -2034,7 +2036,7 @@ public static class PhysicsDiagnostics /// /// /// Initial state from ACDREAM_PROBE_STEP_WALK=1. One-shot - /// diagnostic; no DebugPanel mirror until the root cause is identified. + /// diagnostic. /// /// public static bool ProbeStepWalkEnabled { get; set; } = diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs index d73b5979..0eaa5fee 100644 --- a/src/AcDream.Core/Physics/PhysicsEngine.cs +++ b/src/AcDream.Core/Physics/PhysicsEngine.cs @@ -2316,7 +2316,7 @@ public sealed class PhysicsEngine // L.2a slice 1 (2026-05-12): general-purpose resolver probe. // One line per call when PhysicsDiagnostics.ProbeResolveEnabled // is set (env var ACDREAM_PROBE_RESOLVE=1 at startup, or the - // DebugPanel checkbox flipped at runtime). Captures every + // property assigned at runtime). Captures every // dimension L.2 cares about: input/output position, input/output // cell, ok-vs-partial, grounded-in vs contact-out, contact-plane // status, wall normal if hit, walkable polygon valid. Zero cost diff --git a/src/AcDream.Core/Rendering/CameraDiagnostics.cs b/src/AcDream.Core/Rendering/CameraDiagnostics.cs index 0ceb5eef..02a8d448 100644 --- a/src/AcDream.Core/Rendering/CameraDiagnostics.cs +++ b/src/AcDream.Core/Rendering/CameraDiagnostics.cs @@ -6,7 +6,8 @@ namespace AcDream.Core.Rendering; /// Runtime-tunable knobs for the retail-faithful chase camera. Mirrors /// the pattern: /// static fields seeded from env vars at process start, runtime-settable -/// via property setters that the DebugPanel writes to. +/// via property setters. (#434: the DebugPanel that used to write them has +/// been unreachable since Campaign V slice V11.) /// /// /// Spec: docs/superpowers/specs/2026-05-18-retail-chase-camera-design.md. @@ -21,7 +22,7 @@ public static class CameraDiagnostics /// AcDream.App.Rendering.ChaseCamera rigid-follow camera is. /// Initial state from ACDREAM_RETAIL_CHASE — default-on if /// unset, off only when explicitly set to "0". The legacy - /// camera stays available via the DebugPanel toggle pending the + /// camera stays available by assigning this property pending the /// follow-up deletion commit. /// public static bool UseRetailChaseCamera { get; set; } = diff --git a/src/AcDream.Core/Rendering/RenderingDiagnostics.cs b/src/AcDream.Core/Rendering/RenderingDiagnostics.cs index 8c455cf5..578f9246 100644 --- a/src/AcDream.Core/Rendering/RenderingDiagnostics.cs +++ b/src/AcDream.Core/Rendering/RenderingDiagnostics.cs @@ -8,9 +8,11 @@ namespace AcDream.Core.Rendering; /// /// 2026-05-19 — runtime-toggleable diagnostic flags for the indoor cell /// rendering pipeline. Initialized from env vars at process start; -/// flippable at runtime via the DebugPanel mirror. Log call sites read -/// these statics so a checkbox toggle takes effect on the next frame -/// without relaunching. +/// flippable at runtime by direct assignment. Log call sites read these +/// statics so a change takes effect on the next frame without relaunching. +/// (#434: these used to have a DebugPanel checkbox mirror. That panel has +/// been unreachable since Campaign V slice V11 removed its ImGui host, so +/// every flag here is startup-or-assignment only.) /// /// /// Mirrors the L.2a @@ -90,8 +92,8 @@ public static class RenderingDiagnostics /// Initial state from ACDREAM_PROBE_VIS=1. /// /// Phase U.2d (2026-05-30) repurposed this flag from the abandoned A8 - /// two-pipe stencil pass to the Phase U unified pipeline. The env var name + - /// the DebugPanel mirror (DebugVM.ProbeVisibility) are unchanged. + /// two-pipe stencil pass to the Phase U unified pipeline. The env var name + /// is unchanged (its DebugPanel mirror is gone — #434). /// /// public static bool ProbeVisibilityEnabled { get; set; } = @@ -352,8 +354,8 @@ public static class RenderingDiagnostics /// (the intensity-100 portal purples + the viewer fill off; statics stay); /// 3 = raw vLit visualization in the fragment shader (texture ignored). /// Discriminates lighting-driven stripes (gone at 1/2, visible in the field - /// at 3) from texture/per-pixel machinery (survive 1). Settable for a - /// future DebugPanel mirror. + /// at 3) from texture/per-pixel machinery (survive 1). Settable at + /// runtime by direct assignment. /// public static int LightDebugMode { get; set; } = int.TryParse( @@ -771,7 +773,7 @@ public static class RenderingDiagnostics /// FrameProfiler's own class doc. Every backend reports GPU time /// through FrameProfiler.RecordGpuSample. /// Initial state from ACDREAM_FRAME_PROF=1; runtime-toggleable - /// via the DebugPanel mirror (DebugVM.FrameProf). + /// by direct assignment (its DebugPanel mirror is gone — #434). /// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §5. /// public static bool FrameProfEnabled { get; set; } = diff --git a/src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs b/src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs deleted file mode 100644 index 01292581..00000000 --- a/src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs +++ /dev/null @@ -1,340 +0,0 @@ -using System.Numerics; - -namespace AcDream.UI.Abstractions.Panels.Debug; - -/// -/// The Phase I.2 debug panel — single ImGui window with collapsing-header -/// sections that replace the old custom DebugOverlay's six floating -/// panels (Info / Stats / Help / Compass / Chat / Event) plus the toast -/// surface. Reads through so values are always live. -/// -/// -/// Layout: Player Info, Performance, Compass, Help, Combat events, Recent -/// toasts, Diagnostics. Each section is a CollapsingHeader; -/// importance-ranked sections default open, niche ones default closed. -/// -/// -/// -/// Reuses the I.1 widget extensions only; never imports a backend -/// namespace. Same constraints as VitalsPanel and ChatPanel. -/// -/// -public sealed class DebugPanel : IPanel -{ - private readonly DebugVM _vm; - - public DebugPanel(DebugVM vm) - { - _vm = vm ?? throw new ArgumentNullException(nameof(vm)); - } - - /// - public string Id => "acdream.debug"; - - /// - public string Title => "Debug"; - - /// - public bool IsVisible { get; set; } = true; - - /// - /// 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. - /// - 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"), - }; - - /// - 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; - - // MP0 (2026-07-05): permanent frame profiler toggle — not a - // throwaway investigation probe, so it lives with the other - // always-available diagnostics rather than a dated section. - bool frameProf = _vm.FrameProf; - if (r.Checkbox("Frame profiler ([frame-prof])", ref frameProf)) _vm.FrameProf = frameProf; - - // ── 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]; - } -} diff --git a/src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs b/src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs deleted file mode 100644 index 09330c68..00000000 --- a/src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs +++ /dev/null @@ -1,548 +0,0 @@ -using System.Numerics; -using AcDream.Core.Combat; -using AcDream.Core.Physics; -using AcDream.Core.Rendering; - -namespace AcDream.UI.Abstractions.Panels.Debug; - -/// -/// Severity tag for a single combat-event line in the -/// ring. The panel reads this to pick -/// a TextColored rgba per row (yellow info / red warn / deep-red -/// error). Mirrors the same tri-tone the chat panel uses for combat -/// (Phase I.7). -/// -public enum CombatEventKind -{ - /// You dealt damage / landed a hit. Yellow. - Info, - /// An incoming hit you evaded. Red. - Warn, - /// You took damage. Deep red. - Error, -} - -/// -/// Single typed entry in the combat-events ring. -/// is captured at append time so a future panel revision can fade old -/// entries; for I.2 the panel just renders the text + rgba. -/// -public readonly record struct CombatEventLine( - DateTime Timestamp, - CombatEventKind Kind, - string Text); - -/// -/// Severity tag for a transient toast message. Mirrors -/// but lives in its own enum so the toast -/// surface can grow (e.g. an "OK" green) without dragging the combat -/// surface along. -/// -public enum ToastKind -{ - Info, - Warn, - Error, -} - -/// Single transient toast message kept in the recent-toasts ring. -public readonly record struct ToastMessage( - DateTime Timestamp, - ToastKind Kind, - string Text); - -/// -/// ViewModel for the Phase I.2 . Read-through -/// (no caching): every property forwards to a Func<T> that -/// the host (GameWindow) wires up at construction. Internal -/// state is limited to (a) the combat-event ring buffer, populated via a -/// self-subscription to 's typed events -/// (replacing the old DebugOverlay.BindCombat); (b) the toast -/// ring; (c) the diagnostic-flag bools the panel exposes as checkboxes. -/// -/// -/// Constructor explosion is intentional and acceptable here — the VM -/// lives entirely inside the AcDream.App composition root, not in any -/// plugin-facing surface. A nicer abstraction can come later if more -/// debug panels appear. -/// -/// -public sealed class DebugVM : IDisposable -{ - /// Maximum number of combat-event lines kept in the ring. - public const int MaxCombatEvents = 25; - - /// Maximum number of recent toast messages kept in the ring. - public const int MaxRecentToasts = 25; - - private readonly Func _getPlayerPosition; - private readonly Func _getPlayerHeadingDeg; - private readonly Func _getPlayerCellId; - private readonly Func _getPlayerOnGround; - private readonly Func _getInPlayerMode; - private readonly Func _getInFlyMode; - private readonly Func _getVerticalVelocity; - private readonly Func _getEntityCount; - private readonly Func _getAnimatedCount; - private readonly Func _getLandblocksVisible; - private readonly Func _getLandblocksTotal; - private readonly Func _getShadowObjectCount; - private readonly Func _getNearestObjDist; - private readonly Func _getNearestObjLabel; - private readonly Func _getColliding; - private readonly Func _getDebugWireframes; - private readonly Func _getStreamingRadius; - private readonly Func _getMouseSensitivity; - private readonly Func _getChaseDistance; - private readonly Func _getRmbOrbit; - private readonly Func _getHourName; - private readonly Func _getDayFraction; - private readonly Func _getWeather; - private readonly Func _getActiveLights; - private readonly Func _getRegisteredLights; - private readonly Func _getParticleCount; - private readonly Func _getFps; - private readonly Func _getFrameMs; - private readonly CombatState _combat; - private bool _disposed; - - private readonly Queue _combatEvents = new(); - private readonly Queue _toasts = new(); - - /// - /// Build a VM bound to live data sources. Every Func is read - /// per-frame by the panel — pass closures that resolve to the - /// authoritative source on each call so the panel always sees fresh - /// state. - /// - public DebugVM( - Func getPlayerPosition, - Func getPlayerHeadingDeg, - Func getPlayerCellId, - Func getPlayerOnGround, - Func getInPlayerMode, - Func getInFlyMode, - Func getVerticalVelocity, - Func getEntityCount, - Func getAnimatedCount, - Func getLandblocksVisible, - Func getLandblocksTotal, - Func getShadowObjectCount, - Func getNearestObjDist, - Func getNearestObjLabel, - Func getColliding, - Func getDebugWireframes, - Func getStreamingRadius, - Func getMouseSensitivity, - Func getChaseDistance, - Func getRmbOrbit, - Func getHourName, - Func getDayFraction, - Func getWeather, - Func getActiveLights, - Func getRegisteredLights, - Func getParticleCount, - Func getFps, - Func getFrameMs, - CombatState combat) - { - _combat = combat ?? throw new ArgumentNullException(nameof(combat)); - _getPlayerPosition = getPlayerPosition ?? throw new ArgumentNullException(nameof(getPlayerPosition)); - _getPlayerHeadingDeg = getPlayerHeadingDeg ?? throw new ArgumentNullException(nameof(getPlayerHeadingDeg)); - _getPlayerCellId = getPlayerCellId ?? throw new ArgumentNullException(nameof(getPlayerCellId)); - _getPlayerOnGround = getPlayerOnGround ?? throw new ArgumentNullException(nameof(getPlayerOnGround)); - _getInPlayerMode = getInPlayerMode ?? throw new ArgumentNullException(nameof(getInPlayerMode)); - _getInFlyMode = getInFlyMode ?? throw new ArgumentNullException(nameof(getInFlyMode)); - _getVerticalVelocity = getVerticalVelocity ?? throw new ArgumentNullException(nameof(getVerticalVelocity)); - _getEntityCount = getEntityCount ?? throw new ArgumentNullException(nameof(getEntityCount)); - _getAnimatedCount = getAnimatedCount ?? throw new ArgumentNullException(nameof(getAnimatedCount)); - _getLandblocksVisible = getLandblocksVisible ?? throw new ArgumentNullException(nameof(getLandblocksVisible)); - _getLandblocksTotal = getLandblocksTotal ?? throw new ArgumentNullException(nameof(getLandblocksTotal)); - _getShadowObjectCount = getShadowObjectCount ?? throw new ArgumentNullException(nameof(getShadowObjectCount)); - _getNearestObjDist = getNearestObjDist ?? throw new ArgumentNullException(nameof(getNearestObjDist)); - _getNearestObjLabel = getNearestObjLabel ?? throw new ArgumentNullException(nameof(getNearestObjLabel)); - _getColliding = getColliding ?? throw new ArgumentNullException(nameof(getColliding)); - _getDebugWireframes = getDebugWireframes ?? throw new ArgumentNullException(nameof(getDebugWireframes)); - _getStreamingRadius = getStreamingRadius ?? throw new ArgumentNullException(nameof(getStreamingRadius)); - _getMouseSensitivity = getMouseSensitivity ?? throw new ArgumentNullException(nameof(getMouseSensitivity)); - _getChaseDistance = getChaseDistance ?? throw new ArgumentNullException(nameof(getChaseDistance)); - _getRmbOrbit = getRmbOrbit ?? throw new ArgumentNullException(nameof(getRmbOrbit)); - _getHourName = getHourName ?? throw new ArgumentNullException(nameof(getHourName)); - _getDayFraction = getDayFraction ?? throw new ArgumentNullException(nameof(getDayFraction)); - _getWeather = getWeather ?? throw new ArgumentNullException(nameof(getWeather)); - _getActiveLights = getActiveLights ?? throw new ArgumentNullException(nameof(getActiveLights)); - _getRegisteredLights = getRegisteredLights ?? throw new ArgumentNullException(nameof(getRegisteredLights)); - _getParticleCount = getParticleCount ?? throw new ArgumentNullException(nameof(getParticleCount)); - _getFps = getFps ?? throw new ArgumentNullException(nameof(getFps)); - _getFrameMs = getFrameMs ?? throw new ArgumentNullException(nameof(getFrameMs)); - - // Self-subscribe to combat events. Each one becomes a typed entry - // in the ring; the panel renders them in TextColored. Replaces - // the old DebugOverlay.BindCombat side-channel. - _combat.DamageTaken += OnDamageTaken; - _combat.DamageDealtAccepted += OnDamageDealt; - _combat.EvadedIncoming += OnEvadedIncoming; - _combat.MissedOutgoing += OnMissedOutgoing; - _combat.AttackDone += OnAttackDone; - _combat.KillLanded += OnKillLanded; - } - - // ── Read-through value surfaces ─────────────────────────────────── - - public Vector3 PlayerPosition => _getPlayerPosition(); - public float HeadingDeg => _getPlayerHeadingDeg(); - public uint CellId => _getPlayerCellId(); - public bool OnGround => _getPlayerOnGround(); - public bool InPlayerMode => _getInPlayerMode(); - public bool InFlyMode => _getInFlyMode(); - public float VerticalVelocity => _getVerticalVelocity(); - public int EntityCount => _getEntityCount(); - public int AnimatedCount => _getAnimatedCount(); - public int LandblocksVisible => _getLandblocksVisible(); - public int LandblocksTotal => _getLandblocksTotal(); - public int ShadowObjectCount => _getShadowObjectCount(); - public float NearestObjDist => _getNearestObjDist(); - public string NearestObjLabel => _getNearestObjLabel(); - public bool Colliding => _getColliding(); - public bool DebugWireframes => _getDebugWireframes(); - public int StreamingRadius => _getStreamingRadius(); - public float MouseSensitivity => _getMouseSensitivity(); - public float ChaseDistance => _getChaseDistance(); - public bool RmbOrbit => _getRmbOrbit(); - public string HourName => _getHourName(); - public float DayFraction => _getDayFraction(); - public string Weather => _getWeather(); - public int ActiveLights => _getActiveLights(); - public int RegisteredLights => _getRegisteredLights(); - public int ParticleCount => _getParticleCount(); - public float Fps => _getFps(); - public float FrameMs => _getFrameMs(); - - // ── Diagnostic toggles (env-var-style runtime flags) ─────────────── - - /// Mirror of ACDREAM_DUMP_MOTION; flipped at runtime via the panel. - public bool DumpMotion { get; set; } - /// Mirror of ACDREAM_DUMP_VITALS. - public bool DumpVitals { get; set; } - /// Mirror of ACDREAM_DUMP_OPCODES. - public bool DumpOpcodes { get; set; } - /// Mirror of ACDREAM_DUMP_SKY. - public bool DumpSky { get; set; } - - // L.2a slice 1 (2026-05-12): unlike DumpMotion/Vitals/Opcodes/Sky - // above (which are display-only mirrors of sticky-at-startup env - // vars), these forward directly to the PhysicsDiagnostics statics, - // so checkbox toggles take effect on the next physics resolve. - /// - /// Runtime mirror of PhysicsDiagnostics.ProbeResolveEnabled - /// (env var ACDREAM_PROBE_RESOLVE). Toggling here flips the - /// resolver probe live — no relaunch required. - /// - public bool ProbeResolve - { - get => PhysicsDiagnostics.ProbeResolveEnabled; - set => PhysicsDiagnostics.ProbeResolveEnabled = value; - } - - /// - /// Runtime mirror of PhysicsDiagnostics.ProbeCellEnabled - /// (env var ACDREAM_PROBE_CELL). Toggling here flips the - /// cell-transit probe live. - /// - public bool ProbeCell - { - get => PhysicsDiagnostics.ProbeCellEnabled; - set => PhysicsDiagnostics.ProbeCellEnabled = value; - } - - /// - /// L.2d slice 1 (2026-05-13). Runtime mirror of - /// PhysicsDiagnostics.ProbeBuildingEnabled (env var - /// ACDREAM_PROBE_BUILDING). Toggling here flips the per-hit - /// [resolve-bldg] diagnostic + the registration-time - /// [entity-source] log lines. Heavy when enabled — emits one - /// multi-line entry per BSP hit per physics tick. - /// - public bool ProbeBuilding - { - get => PhysicsDiagnostics.ProbeBuildingEnabled; - set => PhysicsDiagnostics.ProbeBuildingEnabled = value; - } - - /// - /// B.6 slice 1 (2026-05-14). Runtime mirror of - /// PhysicsDiagnostics.ProbeAutoWalkEnabled (env var - /// ACDREAM_PROBE_AUTOWALK). Toggling here flips the - /// [autowalk-out] / [autowalk-mt] / [autowalk-up] - /// trace used to characterize ACE's behavior during a server- - /// initiated auto-walk (issue #63). Low volume when off — only the - /// local player's events are filtered through the probe. - /// - public bool ProbeAutoWalk - { - get => PhysicsDiagnostics.ProbeAutoWalkEnabled; - set => PhysicsDiagnostics.ProbeAutoWalkEnabled = value; - } - - /// - /// Runtime mirror of RenderingDiagnostics.FrameProfEnabled - /// (env var ACDREAM_FRAME_PROF). Toggling here starts/stops the - /// [frame-prof] 5-second report live — no relaunch required. - /// - public bool FrameProf - { - get => RenderingDiagnostics.FrameProfEnabled; - set => RenderingDiagnostics.FrameProfEnabled = value; - } - - // ── Indoor rendering diagnostics (2026-05-19) ─────────────────── - // Mirror RenderingDiagnostics statics so DebugPanel checkbox toggles - // take effect on the next render frame without relaunching. - - /// - /// Runtime mirror of RenderingDiagnostics.ProbeIndoorWalkEnabled - /// (env var ACDREAM_PROBE_INDOOR_WALK). - /// - public bool ProbeIndoorWalk - { - get => RenderingDiagnostics.ProbeIndoorWalkEnabled; - set => RenderingDiagnostics.ProbeIndoorWalkEnabled = value; - } - - /// - /// Runtime mirror of RenderingDiagnostics.ProbeIndoorLookupEnabled - /// (env var ACDREAM_PROBE_INDOOR_LOOKUP). - /// - public bool ProbeIndoorLookup - { - get => RenderingDiagnostics.ProbeIndoorLookupEnabled; - set => RenderingDiagnostics.ProbeIndoorLookupEnabled = value; - } - - /// - /// Runtime mirror of RenderingDiagnostics.ProbeIndoorUploadEnabled - /// (env var ACDREAM_PROBE_INDOOR_UPLOAD). - /// - public bool ProbeIndoorUpload - { - get => RenderingDiagnostics.ProbeIndoorUploadEnabled; - set => RenderingDiagnostics.ProbeIndoorUploadEnabled = value; - } - - /// - /// Runtime mirror of RenderingDiagnostics.ProbeIndoorXformEnabled - /// (env var ACDREAM_PROBE_INDOOR_XFORM). - /// - public bool ProbeIndoorXform - { - get => RenderingDiagnostics.ProbeIndoorXformEnabled; - set => RenderingDiagnostics.ProbeIndoorXformEnabled = value; - } - - /// - /// Runtime mirror of RenderingDiagnostics.ProbeIndoorCullEnabled - /// (env var ACDREAM_PROBE_INDOOR_CULL). - /// - public bool ProbeIndoorCull - { - get => RenderingDiagnostics.ProbeIndoorCullEnabled; - set => RenderingDiagnostics.ProbeIndoorCullEnabled = value; - } - - /// - /// Phase A8 (2026-05-25). Runtime mirror of - /// RenderingDiagnostics.ProbeVisibilityEnabled - /// (env var ACDREAM_PROBE_VIS). - /// - public bool ProbeVisibility - { - get => RenderingDiagnostics.ProbeVisibilityEnabled; - set => RenderingDiagnostics.ProbeVisibilityEnabled = value; - } - - /// - /// Indoor walking Phase 1 (2026-05-19). Runtime mirror of - /// PhysicsDiagnostics.ProbeIndoorBspEnabled (env var - /// ACDREAM_PROBE_INDOOR_BSP). Toggling here flips the - /// [indoor-bsp] probe live — no relaunch required. - /// Physics-side companion to the five render-side - /// ProbeIndoor* mirrors directly above. - /// - public bool ProbeIndoorBsp - { - get => PhysicsDiagnostics.ProbeIndoorBspEnabled; - set => PhysicsDiagnostics.ProbeIndoorBspEnabled = value; - } - - /// - /// Phase A6.P1 cdb probe spike (2026-05-21). Runtime mirror of - /// (env var - /// ACDREAM_PROBE_PUSH_BACK). Toggling here flips the three - /// [push-back] emission sites live — no relaunch required. - /// - public bool ProbePushBack - { - get => PhysicsDiagnostics.ProbePushBackEnabled; - set => PhysicsDiagnostics.ProbePushBackEnabled = value; - } - - /// - /// Runtime mirror of RenderingDiagnostics.IndoorAll — toggles all - /// five indoor probes together. No dedicated env var; set any individual - /// probe env var or use ACDREAM_PROBE_INDOOR_ALL to initialize - /// all five flags on at startup. - /// - public bool ProbeIndoorAll - { - get => RenderingDiagnostics.IndoorAll; - set => RenderingDiagnostics.IndoorAll = value; - } - - // ── Chase camera tunables (forward to CameraDiagnostics) ────────── - - /// Runtime mirror of . - public bool UseRetailChaseCamera - { - get => CameraDiagnostics.UseRetailChaseCamera; - set => CameraDiagnostics.UseRetailChaseCamera = value; - } - - /// Runtime mirror of . - public bool CameraAlignToSlope - { - get => CameraDiagnostics.AlignToSlope; - set => CameraDiagnostics.AlignToSlope = value; - } - - /// Runtime mirror of . - public float CameraTranslationStiffness - { - get => CameraDiagnostics.TranslationStiffness; - set => CameraDiagnostics.TranslationStiffness = value; - } - - /// Runtime mirror of . - public float CameraRotationStiffness - { - get => CameraDiagnostics.RotationStiffness; - set => CameraDiagnostics.RotationStiffness = value; - } - - /// Runtime mirror of . - public float CameraMouseLowPassWindowSec - { - get => CameraDiagnostics.MouseLowPassWindowSec; - set => CameraDiagnostics.MouseLowPassWindowSec = value; - } - - /// Runtime mirror of . - public float CameraAdjustmentSpeed - { - get => CameraDiagnostics.CameraAdjustmentSpeed; - set => CameraDiagnostics.CameraAdjustmentSpeed = value; - } - - // ── Action hooks invoked by panel buttons ────────────────────────── - - /// - /// Cycle the time-of-day debug override (matches the old F7 - /// behavior — none → midnight → dawn → noon → dusk → none). Wired - /// by GameWindow; null when no host is available (tests). - /// - public Action? CycleTimeOfDay { get; set; } - - /// - /// Cycle the weather-kind debug override (matches the old F10 - /// behavior — clear → overcast → rain → snow → storm). - /// - public Action? CycleWeather { get; set; } - - /// - /// Toggle the collision-wires debug renderer. Same effect as the - /// old F2 keybind, which we keep as a hotkey alias. - /// - public Action? ToggleCollisionWires { get; set; } - - /// - /// Phase K.2 — toggle the free-fly camera. Lets a user opt out of - /// the auto-entered chase camera (e.g. to inspect a remote part of - /// the world without the player following) without needing to find - /// the Ctrl+F* debug binding. Wired by GameWindow to the - /// same routine the legacy F-key fly toggle invokes. - /// - public Action? ToggleFlyMode { get; set; } - - // ── Combat event ring + toast ring ───────────────────────────────── - - /// - /// Snapshot view of the combat-event ring. Oldest-first; the panel - /// can iterate and render each line through TextColored - /// based on . - /// - public IReadOnlyCollection CombatEvents => _combatEvents; - - /// Snapshot view of the recent-toasts ring (oldest-first). - public IReadOnlyCollection RecentToasts => _toasts; - - /// - /// Append a toast message to the ring. Cap at - /// ; oldest entries drop. The panel's - /// "Recent toasts" section reads this; no on-screen flash for I.2. - /// - public void AddToast(string text, ToastKind kind = ToastKind.Info) - { - if (string.IsNullOrEmpty(text)) return; - _toasts.Enqueue(new ToastMessage(DateTime.UtcNow, kind, text)); - while (_toasts.Count > MaxRecentToasts) - _toasts.Dequeue(); - } - - private void Push(CombatEventKind kind, string text) - { - _combatEvents.Enqueue(new CombatEventLine(DateTime.UtcNow, kind, text)); - while (_combatEvents.Count > MaxCombatEvents) - _combatEvents.Dequeue(); - } - - public void Dispose() - { - if (_disposed) - return; - - _combat.DamageTaken -= OnDamageTaken; - _combat.DamageDealtAccepted -= OnDamageDealt; - _combat.EvadedIncoming -= OnEvadedIncoming; - _combat.MissedOutgoing -= OnMissedOutgoing; - _combat.AttackDone -= OnAttackDone; - _combat.KillLanded -= OnKillLanded; - _disposed = true; - } - - private void OnDamageTaken(CombatState.DamageIncoming damage) => - Push( - CombatEventKind.Error, - $"<< {damage.AttackerName} hit you for {damage.Damage}" + - (damage.Critical ? " CRIT!" : string.Empty)); - - private void OnDamageDealt(CombatState.DamageDealt damage) => - Push( - CombatEventKind.Info, - $">> you hit {damage.DefenderName} for {damage.Damage}"); - - private void OnEvadedIncoming(string attacker) => - Push(CombatEventKind.Warn, $"<< {attacker}'s attack missed you"); - - private void OnMissedOutgoing(string defender) => - Push(CombatEventKind.Info, $">> your attack missed {defender}"); - - private void OnAttackDone(uint _, uint weenieError) - { - if (weenieError != 0) - Push( - CombatEventKind.Error, - $"!! attack failed (error 0x{weenieError:X})"); - } - - private void OnKillLanded(string victim, uint _) => - Push(CombatEventKind.Info, $"** you killed {victim}"); -} diff --git a/tests/AcDream.App.Tests/Combat/CombatFeedbackSlotTests.cs b/tests/AcDream.App.Tests/Combat/CombatFeedbackSlotTests.cs index 4fe8a669..2f2e6d9e 100644 --- a/tests/AcDream.App.Tests/Combat/CombatFeedbackSlotTests.cs +++ b/tests/AcDream.App.Tests/Combat/CombatFeedbackSlotTests.cs @@ -1,7 +1,4 @@ -using System.Numerics; using AcDream.App.Combat; -using AcDream.Core.Combat; -using AcDream.UI.Abstractions.Panels.Debug; namespace AcDream.App.Tests.Combat; @@ -11,49 +8,41 @@ public sealed class CombatFeedbackSlotTests public void ExpectedOwnerUnbindCannotClearReplacement() { var slot = new CombatFeedbackSlot(); - using DebugVM first = CreateViewModel(); - using DebugVM second = CreateViewModel(); + List first = []; + List second = []; + Action firstTarget = first.Add; + Action secondTarget = second.Add; - slot.Bind(first); - slot.Unbind(second); + slot.Bind(firstTarget); + slot.Unbind(secondTarget); slot.Show("first"); - Assert.Single(first.RecentToasts); - Assert.Empty(second.RecentToasts); + Assert.Equal(["first"], first); + Assert.Empty(second); - slot.Unbind(first); - slot.Bind(second); + slot.Unbind(firstTarget); + slot.Bind(secondTarget); slot.Show("second"); - Assert.Single(second.RecentToasts); + Assert.Equal(["second"], second); } - private static DebugVM CreateViewModel() => new( - static () => Vector3.Zero, - static () => 0, - static () => 0, - static () => false, - static () => false, - static () => false, - static () => 0, - static () => 0, - static () => 0, - static () => 0, - static () => 0, - static () => 0, - static () => float.PositiveInfinity, - static () => "-", - static () => false, - static () => false, - static () => 0, - static () => 1, - static () => 0, - static () => false, - static () => "0", - static () => 0, - static () => "Clear", - static () => 0, - static () => 0, - static () => 0, - static () => 60, - static () => 16.7f, - new CombatState()); + [Fact] + public void RebindingADifferentTargetWhileBoundIsRejected() + { + var slot = new CombatFeedbackSlot(); + slot.Bind(_ => { }); + + Assert.Throws(() => slot.Bind(_ => { })); + } + + [Fact] + public void AnUnboundSlotDropsItsMessages() + { + // #434/#436: this is the shipped behavior, not an aspiration — + // nothing binds the slot in production, so combat refusal text + // ("No monster target") is discarded. Pinned so the day it gets a + // real binder, this test is the one that has to change. + var slot = new CombatFeedbackSlot(); + + slot.Show("dropped"); + } } diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Debug/DebugVMTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Debug/DebugVMTests.cs deleted file mode 100644 index 3028cfd0..00000000 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Debug/DebugVMTests.cs +++ /dev/null @@ -1,327 +0,0 @@ -using System.Numerics; -using AcDream.Core.Combat; -using AcDream.Core.Physics; -using AcDream.UI.Abstractions.Panels.Debug; - -namespace AcDream.UI.Abstractions.Tests.Panels.Debug; - -/// -/// Tests for the Phase I.2 — read-through ViewModel -/// for the migrated debug panel. Verifies the combat-event subscription -/// (replacing the old DebugOverlay.BindCombat), the toast ring -/// cap, and that the diagnostic-flag bools round-trip without affecting -/// the rest of the VM. -/// -public sealed class DebugVMTests -{ - [Fact] - public void DisposeDetachesEveryCombatSubscriptionExactlyOnce() - { - var combat = new CombatState(); - DebugVM vm = NewVm(combat); - combat.OnKillerNotification("first", 1); - Assert.Single(vm.CombatEvents); - - vm.Dispose(); - vm.Dispose(); - combat.OnKillerNotification("second", 2); - combat.OnAttackDone(3, 4); - - Assert.Single(vm.CombatEvents); - } - - /// - /// Build a minimal with safe defaults for every - /// constructor source. Tests that don't care about a particular source - /// just leave it stubbed out. - /// - private static DebugVM NewVm(CombatState? combat = null) - { - combat ??= new CombatState(); - return new DebugVM( - getPlayerPosition: () => Vector3.Zero, - getPlayerHeadingDeg: () => 0f, - getPlayerCellId: () => 0u, - getPlayerOnGround: () => true, - getInPlayerMode: () => false, - getInFlyMode: () => false, - getVerticalVelocity: () => 0f, - getEntityCount: () => 0, - getAnimatedCount: () => 0, - getLandblocksVisible: () => 0, - getLandblocksTotal: () => 0, - getShadowObjectCount: () => 0, - getNearestObjDist: () => float.PositiveInfinity, - getNearestObjLabel: () => "-", - getColliding: () => false, - getDebugWireframes: () => false, - getStreamingRadius: () => 2, - getMouseSensitivity: () => 1f, - getChaseDistance: () => 0f, - getRmbOrbit: () => false, - getHourName: () => "Dawnsong", - getDayFraction: () => 0.25f, - getWeather: () => "Clear", - getActiveLights: () => 0, - getRegisteredLights: () => 0, - getParticleCount: () => 0, - getFps: () => 60f, - getFrameMs: () => 16.7f, - combat: combat); - } - - [Fact] - public void Constructor_ThrowsOnNullCombat() - { - Assert.Throws(() => new DebugVM( - getPlayerPosition: () => Vector3.Zero, - getPlayerHeadingDeg: () => 0f, - getPlayerCellId: () => 0u, - getPlayerOnGround: () => true, - getInPlayerMode: () => false, - getInFlyMode: () => false, - getVerticalVelocity: () => 0f, - getEntityCount: () => 0, - getAnimatedCount: () => 0, - getLandblocksVisible: () => 0, - getLandblocksTotal: () => 0, - getShadowObjectCount: () => 0, - getNearestObjDist: () => 0f, - getNearestObjLabel: () => "-", - getColliding: () => false, - getDebugWireframes: () => false, - getStreamingRadius: () => 0, - getMouseSensitivity: () => 1f, - getChaseDistance: () => 0f, - getRmbOrbit: () => false, - getHourName: () => "", - getDayFraction: () => 0f, - getWeather: () => "", - getActiveLights: () => 0, - getRegisteredLights: () => 0, - getParticleCount: () => 0, - getFps: () => 0f, - getFrameMs: () => 0f, - combat: null!)); - } - - [Fact] - public void ReadThrough_PullsLiveValuesPerAccess_NoCache() - { - // The VM does NOT cache; every property read goes through the - // backing Func. Mutating an external box between two reads - // must surface immediately. - int counter = 0; - var vm = new DebugVM( - getPlayerPosition: () => Vector3.Zero, - getPlayerHeadingDeg: () => 0f, - getPlayerCellId: () => 0u, - getPlayerOnGround: () => true, - getInPlayerMode: () => false, - getInFlyMode: () => false, - getVerticalVelocity: () => 0f, - getEntityCount: () => ++counter, - getAnimatedCount: () => 0, - getLandblocksVisible: () => 0, - getLandblocksTotal: () => 0, - getShadowObjectCount: () => 0, - getNearestObjDist: () => 0f, - getNearestObjLabel: () => "-", - getColliding: () => false, - getDebugWireframes: () => false, - getStreamingRadius: () => 0, - getMouseSensitivity: () => 1f, - getChaseDistance: () => 0f, - getRmbOrbit: () => false, - getHourName: () => "", - getDayFraction: () => 0f, - getWeather: () => "", - getActiveLights: () => 0, - getRegisteredLights: () => 0, - getParticleCount: () => 0, - getFps: () => 0f, - getFrameMs: () => 0f, - combat: new CombatState()); - - Assert.Equal(1, vm.EntityCount); - Assert.Equal(2, vm.EntityCount); - Assert.Equal(3, vm.EntityCount); - } - - [Fact] - public void DamageTaken_AppendsErrorEvent() - { - var combat = new CombatState(); - var vm = NewVm(combat); - - combat.OnVictimNotification( - attackerName: "Drudge", attackerGuid: 0x10u, - damageType: 0u, damage: 12u, hitQuadrant: 0u, - critical: 0u, attackType: 0u); - - var events = vm.CombatEvents.ToList(); - Assert.Single(events); - Assert.Equal(CombatEventKind.Error, events[0].Kind); - Assert.Contains("Drudge", events[0].Text); - Assert.Contains("12", events[0].Text); - } - - [Fact] - public void DamageDealt_AppendsInfoEvent() - { - var combat = new CombatState(); - var vm = NewVm(combat); - - combat.OnAttackerNotification( - defenderName: "Drudge", damageType: 0u, - damage: 8u, damagePercent: 0.1f); - - var events = vm.CombatEvents.ToList(); - Assert.Single(events); - Assert.Equal(CombatEventKind.Info, events[0].Kind); - Assert.Contains("Drudge", events[0].Text); - } - - [Fact] - public void EvadedIncoming_AppendsWarnEvent() - { - var combat = new CombatState(); - var vm = NewVm(combat); - - combat.OnEvasionDefenderNotification("Mosswart"); - - var events = vm.CombatEvents.ToList(); - Assert.Single(events); - Assert.Equal(CombatEventKind.Warn, events[0].Kind); - Assert.Contains("Mosswart", events[0].Text); - } - - [Fact] - public void CombatEventRing_CapsAtMax_DropsOldest() - { - var combat = new CombatState(); - var vm = NewVm(combat); - - // Pump well past the cap (25). The oldest entries must drop out. - for (int i = 0; i < 40; i++) - { - combat.OnAttackerNotification( - defenderName: $"Foe{i}", damageType: 0u, - damage: (uint)i, damagePercent: 0.1f); - } - - var events = vm.CombatEvents.ToList(); - Assert.Equal(DebugVM.MaxCombatEvents, events.Count); - // The newest entry must still be present. - Assert.Contains(events, e => e.Text.Contains("Foe39")); - // The oldest must have dropped. - Assert.DoesNotContain(events, e => e.Text.Contains("Foe0,") || e.Text == "Foe0"); - Assert.DoesNotContain(events, e => e.Text.Contains("Foe5")); - } - - [Fact] - public void Toast_RingCapsAtMaxRecent() - { - var vm = NewVm(); - - for (int i = 0; i < 30; i++) - vm.AddToast($"toast-{i}"); - - var toasts = vm.RecentToasts.ToList(); - Assert.Equal(DebugVM.MaxRecentToasts, toasts.Count); - Assert.Contains(toasts, t => t.Text == "toast-29"); - Assert.DoesNotContain(toasts, t => t.Text == "toast-0"); - } - - [Fact] - public void Toast_PreservesKind() - { - var vm = NewVm(); - vm.AddToast("warning!", ToastKind.Warn); - vm.AddToast("error!", ToastKind.Error); - - var toasts = vm.RecentToasts.ToList(); - Assert.Equal(2, toasts.Count); - Assert.Contains(toasts, t => t.Text == "warning!" && t.Kind == ToastKind.Warn); - Assert.Contains(toasts, t => t.Text == "error!" && t.Kind == ToastKind.Error); - } - - [Fact] - public void DiagnosticFlags_DefaultFalse_RoundTripIndependently() - { - var vm = NewVm(); - Assert.False(vm.DumpMotion); - Assert.False(vm.DumpVitals); - Assert.False(vm.DumpOpcodes); - Assert.False(vm.DumpSky); - - vm.DumpMotion = true; - Assert.True(vm.DumpMotion); - Assert.False(vm.DumpVitals); - Assert.False(vm.DumpOpcodes); - Assert.False(vm.DumpSky); - - vm.DumpSky = true; - vm.DumpMotion = false; - Assert.False(vm.DumpMotion); - Assert.True(vm.DumpSky); - } - - [Fact] - public void ToggleFlags_DoNotAffectCombatRing() - { - var combat = new CombatState(); - var vm = NewVm(combat); - - combat.OnAttackerNotification("X", 0u, 1u, 0.1f); - Assert.Single(vm.CombatEvents); - - vm.DumpMotion = true; - vm.DumpVitals = true; - Assert.Single(vm.CombatEvents); - } - - [Fact] - public void ActionHooks_InvokeSuppliedDelegates() - { - // The panel needs to invoke "cycle time", "cycle weather", "toggle - // collision wires" actions when the corresponding Button is - // clicked. The VM exposes these as Action; the panel calls them. - int timeHits = 0, weatherHits = 0, wireHits = 0; - var vm = NewVm(); - vm.CycleTimeOfDay = () => timeHits++; - vm.CycleWeather = () => weatherHits++; - vm.ToggleCollisionWires = () => wireHits++; - - vm.CycleTimeOfDay?.Invoke(); - vm.CycleTimeOfDay?.Invoke(); - vm.CycleWeather?.Invoke(); - vm.ToggleCollisionWires?.Invoke(); - - Assert.Equal(2, timeHits); - Assert.Equal(1, weatherHits); - Assert.Equal(1, wireHits); - } - - [Fact] - public void ProbeIndoorBsp_ForwardsToPhysicsDiagnostics() - { - var originalEnabled = PhysicsDiagnostics.ProbeIndoorBspEnabled; - try - { - var vm = NewVm(); - - vm.ProbeIndoorBsp = true; - Assert.True(PhysicsDiagnostics.ProbeIndoorBspEnabled); - Assert.True(vm.ProbeIndoorBsp); - - vm.ProbeIndoorBsp = false; - Assert.False(PhysicsDiagnostics.ProbeIndoorBspEnabled); - Assert.False(vm.ProbeIndoorBsp); - } - finally - { - PhysicsDiagnostics.ProbeIndoorBspEnabled = originalEnabled; - } - } -}