feat(ui): #15 migrate DebugOverlay to ImGui DebugPanel - 7 collapsing sections + diagnostics toggles
Replaces the 473-LOC custom-StbTrueTypeSharp DebugOverlay with an ImGui-rendered DebugPanel using the I.1 widget extensions. Single window with 7 CollapsingHeader sections; checkboxes are the primary toggle surface; F-keys retained where they invoke real gameplay actions, dropped where they only toggled panels. Pieces: - DebugVM (UI.Abstractions): read-through ViewModel with combat-event ring (cap 25), toast ring (cap 25), 4 diagnostic-flag bools (DumpMotion / DumpVitals / DumpOpcodes / DumpSky), 3 Action hooks (CycleTimeOfDay / CycleWeather / ToggleCollisionWires). Self- subscribes to CombatState.DamageTaken/DealtAccepted/Evaded* / Missed*/AttackDone/KillLanded - replaces the old BindCombat path. - DebugPanel (UI.Abstractions): one ImGui window with sections Player Info, Performance, Compass (text-only - draw-list strip deferred to D.6), Help (BeginTable cheat-sheet), Combat events (TextColored by kind: Info=yellow, Warning=red, Error=deep red), Recent toasts, Diagnostics (Checkboxes for the 4 flags + Buttons for the 3 cycle/toggle actions). - All 28 Snapshot data points covered: Fps, FrameMs, PlayerPos, HeadingDeg, CellId, OnGround, InPlayerMode, InFlyMode, VerticalVelocity, EntityCount, AnimatedCount, LandblocksVisible, LandblocksTotal, ShadowObjectCount, NearestObjDist, NearestObjLabel, Colliding, DebugWireframes, StreamingRadius, MouseSensitivity, ChaseDistance, RmbOrbit, HourName, DayFraction, Weather, ActiveLights, RegisteredLights, ParticleCount. - GameWindow surgery (+252/-165): removed _debugOverlay field + snapshot builder block + Update/Draw calls; added _debugVm / _debugPanel construction in the if (DevToolsEnabled) block; added per-frame nearest-object scan cached for VM closures (zero cost when devtools off); helper methods CycleTimeOfDay / CycleWeather / ToggleCollisionWires / GetDebug* / GetActiveSensitivity. F-key disposition: - F1: repurposed - now toggles whole DebugPanel visibility. - F2: kept - ToggleCollisionWires (also a Button in panel). - F4 / F5 / F6: REMOVED - per-section toggles replaced by CollapsingHeader inside one window. - F7: kept - CycleTimeOfDay (also Button). - F8 / F9: kept - mouse-sensitivity adjust; toasts route to _debugVm.AddToast. - F10: kept - CycleWeather (also Button). DebugOverlay.cs DELETED (473 LOC). TextRenderer + BitmapFont kept alive: UiHost references _debugFont and the future HUD-in-world (D.6) will reuse both. 11 new DebugVM tests covering combat-event-ring subscription, toast ring cap, diagnostic-flag toggles. UI.Abstractions.Tests: 96 -> 107. Solution total: 989 green (243 Core.Net + 639 Core + 107 UI). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3d26c8efde
commit
56037a4471
5 changed files with 1081 additions and 639 deletions
|
|
@ -30,18 +30,29 @@ public sealed class GameWindow : IDisposable
|
|||
private bool _debugCollisionVisible = true;
|
||||
private int _debugDrawLogOnce = 0;
|
||||
|
||||
// On-screen debug HUD — info panel, stats panel, compass, keybind help.
|
||||
// F1/F2/F4/F5/F6 toggle the individual panels (see the key handler).
|
||||
// Null if no system font is available at startup; in that case the HUD
|
||||
// is silently disabled and the rest of the client keeps working.
|
||||
// Phase I.2: the old StbTrueTypeSharp DebugOverlay was deleted in
|
||||
// favor of the ImGui-backed DebugPanel (see _debugVm below). The
|
||||
// TextRenderer + BitmapFont fields stay alive because they're shared
|
||||
// with UiHost and reserved for the future world-space HUD (D.6 —
|
||||
// damage floaters, name plates) where ImGui can't reach into the 3D
|
||||
// scene. They are no longer used for any debug overlay.
|
||||
private TextRenderer? _textRenderer;
|
||||
private BitmapFont? _debugFont;
|
||||
private DebugOverlay? _debugOverlay;
|
||||
// Last-computed perf values so the HUD always has something to show even
|
||||
// though the title-bar FPS is only updated every 0.5s.
|
||||
private double _lastFps = 60.0;
|
||||
private double _lastFrameMs = 16.7;
|
||||
|
||||
// Phase I.2: per-frame counters surfaced through the ImGui DebugPanel
|
||||
// VM closures. Computed once per render pass alongside the frustum
|
||||
// walk + nearest-object scan; the VM closures just read the cached
|
||||
// values. Skipped when DevTools are off (zero cost).
|
||||
private int _lastVisibleLandblocks;
|
||||
private int _lastTotalLandblocks;
|
||||
private float _lastNearestObjDist = float.PositiveInfinity;
|
||||
private string _lastNearestObjLabel = "-";
|
||||
private bool _lastColliding;
|
||||
|
||||
// Phase A.1: streaming fields replacing the one-shot _entities list.
|
||||
private AcDream.App.Streaming.LandblockStreamer? _streamer;
|
||||
private readonly AcDream.App.Streaming.GpuWorldState _worldState = new();
|
||||
|
|
@ -301,6 +312,10 @@ public sealed class GameWindow : IDisposable
|
|||
private AcDream.UI.ImGui.ImGuiBootstrapper? _imguiBootstrap;
|
||||
private AcDream.UI.ImGui.ImGuiPanelHost? _panelHost;
|
||||
private AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? _vitalsVm;
|
||||
// Phase I.2: ImGui debug panel ViewModel. Lives for as long as
|
||||
// _panelHost does. Self-subscribes to CombatState in its ctor, so
|
||||
// disposing isn't required (panel host holds the only ref).
|
||||
private AcDream.UI.Abstractions.Panels.Debug.DebugVM? _debugVm;
|
||||
private static readonly bool DevToolsEnabled =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_DEVTOOLS") == "1";
|
||||
|
||||
|
|
@ -537,85 +552,34 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
else if (key == Key.F1)
|
||||
{
|
||||
if (_debugOverlay is not null)
|
||||
// Phase I.2: F1 now toggles the entire ImGui DebugPanel
|
||||
// visibility. The old per-section toggles (F4/F5/F6) are
|
||||
// gone — sections are collapsing headers inside the
|
||||
// single window now.
|
||||
foreach (var panel in EnumerateDebugPanel())
|
||||
{
|
||||
_debugOverlay.ShowHelpPanel = !_debugOverlay.ShowHelpPanel;
|
||||
_debugOverlay.Toast($"Help {(_debugOverlay.ShowHelpPanel ? "ON" : "OFF")}");
|
||||
panel.IsVisible = !panel.IsVisible;
|
||||
_debugVm?.AddToast($"Debug panel {(panel.IsVisible ? "ON" : "OFF")}");
|
||||
}
|
||||
}
|
||||
else if (key == Key.F2)
|
||||
{
|
||||
_debugCollisionVisible = !_debugCollisionVisible;
|
||||
_debugOverlay?.Toast($"Collision wireframes {(_debugCollisionVisible ? "ON" : "OFF")}");
|
||||
}
|
||||
else if (key == Key.F4)
|
||||
{
|
||||
if (_debugOverlay is not null)
|
||||
{
|
||||
_debugOverlay.ShowInfoPanel = !_debugOverlay.ShowInfoPanel;
|
||||
_debugOverlay.Toast($"Info panel {(_debugOverlay.ShowInfoPanel ? "ON" : "OFF")}");
|
||||
}
|
||||
}
|
||||
else if (key == Key.F5)
|
||||
{
|
||||
if (_debugOverlay is not null)
|
||||
{
|
||||
_debugOverlay.ShowStatsPanel = !_debugOverlay.ShowStatsPanel;
|
||||
_debugOverlay.Toast($"Stats panel {(_debugOverlay.ShowStatsPanel ? "ON" : "OFF")}");
|
||||
}
|
||||
}
|
||||
else if (key == Key.F6)
|
||||
{
|
||||
if (_debugOverlay is not null)
|
||||
{
|
||||
_debugOverlay.ShowCompass = !_debugOverlay.ShowCompass;
|
||||
_debugOverlay.Toast($"Compass {(_debugOverlay.ShowCompass ? "ON" : "OFF")}");
|
||||
}
|
||||
// Real gameplay toggle — keeps the F2 keybind. Same
|
||||
// action is wired into the DebugPanel's
|
||||
// "Toggle collision wires" button via DebugVM.
|
||||
ToggleCollisionWires();
|
||||
}
|
||||
else if (key == Key.F7)
|
||||
{
|
||||
// Phase G.1: cycle debug time-of-day overrides. Useful for
|
||||
// visually verifying the sun arc + keyframe transitions
|
||||
// without waiting 30+ real-time hours. Cycle order:
|
||||
// clear debug → 0.0 (midnight) → 0.25 (dawn)
|
||||
// → 0.5 (noon) → 0.75 (dusk) → clear
|
||||
_timeDebugStep = (_timeDebugStep + 1) % 5;
|
||||
float? pick = _timeDebugStep switch
|
||||
{
|
||||
0 => (float?)null, // server time
|
||||
1 => 0.0f,
|
||||
2 => 0.25f,
|
||||
3 => 0.5f,
|
||||
4 => 0.75f,
|
||||
_ => null,
|
||||
};
|
||||
if (pick.HasValue)
|
||||
{
|
||||
WorldTime.SetDebugTime(pick.Value);
|
||||
_debugOverlay?.Toast($"Time override = {pick.Value:F2}");
|
||||
}
|
||||
else
|
||||
{
|
||||
WorldTime.ClearDebugTime();
|
||||
_debugOverlay?.Toast("Time override cleared");
|
||||
}
|
||||
// Phase I.2: keep F7 as a hotkey alias for the
|
||||
// DebugPanel's "Cycle time of day" button.
|
||||
CycleTimeOfDay();
|
||||
}
|
||||
else if (key == Key.F10)
|
||||
{
|
||||
// Phase G.1: cycle weather kinds manually. Useful for
|
||||
// testing the rain/snow particle systems + storm/light
|
||||
// fog without waiting for the daily RNG to hit.
|
||||
var kinds = new[]
|
||||
{
|
||||
AcDream.Core.World.WeatherKind.Clear,
|
||||
AcDream.Core.World.WeatherKind.Overcast,
|
||||
AcDream.Core.World.WeatherKind.Rain,
|
||||
AcDream.Core.World.WeatherKind.Snow,
|
||||
AcDream.Core.World.WeatherKind.Storm,
|
||||
};
|
||||
_weatherDebugStep = (_weatherDebugStep + 1) % kinds.Length;
|
||||
Weather.ForceWeather(kinds[_weatherDebugStep]);
|
||||
_debugOverlay?.Toast($"Weather = {kinds[_weatherDebugStep]}");
|
||||
// Phase I.2: keep F10 as a hotkey alias for the
|
||||
// DebugPanel's "Cycle weather" button.
|
||||
CycleWeather();
|
||||
}
|
||||
else if (key == Key.F8 || key == Key.F9)
|
||||
{
|
||||
|
|
@ -638,7 +602,7 @@ public sealed class GameWindow : IDisposable
|
|||
else if (modeLabel == "Fly") _sensFly = next;
|
||||
else _sensOrbit = next;
|
||||
|
||||
_debugOverlay?.Toast($"{modeLabel} sens {next:F3}x");
|
||||
_debugVm?.AddToast($"{modeLabel} sens {next:F3}x");
|
||||
}
|
||||
else if (key == Key.Escape)
|
||||
{
|
||||
|
|
@ -857,25 +821,23 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
_debugLines = new DebugLineRenderer(_gl, shadersDir);
|
||||
|
||||
// Debug HUD: load a system monospace font and set up the text overlay.
|
||||
// Skips silently if no font is available (the rest of the client still works).
|
||||
// Phase I.2: load a system monospace font + TextRenderer for the
|
||||
// future world-space HUD (D.6). The custom DebugOverlay is gone;
|
||||
// the ImGui DebugPanel handles all dev surfaces now. These fields
|
||||
// are reserved for future work — currently unused at the renderer
|
||||
// level. Skips silently if no font is available.
|
||||
var fontBytes = BitmapFont.TryLoadSystemMonospaceFont();
|
||||
if (fontBytes is not null)
|
||||
{
|
||||
_debugFont = new BitmapFont(_gl, fontBytes, pixelHeight: 15f, atlasSize: 512);
|
||||
_textRenderer = new TextRenderer(_gl, shadersDir);
|
||||
_debugOverlay = new DebugOverlay(_textRenderer, _debugFont);
|
||||
// Phase F.1/H.1/E.4 visibility: show chat + combat events on screen.
|
||||
_debugOverlay.Chat = Chat;
|
||||
_debugOverlay.Combat = Combat;
|
||||
_debugOverlay.BindCombat(Combat);
|
||||
Console.WriteLine($"debug overlay: loaded {fontBytes.Length / 1024}KB font, " +
|
||||
Console.WriteLine($"world-hud font: loaded {fontBytes.Length / 1024}KB, " +
|
||||
$"atlas {_debugFont.AtlasWidth}x{_debugFont.AtlasHeight}, " +
|
||||
$"lineHeight={_debugFont.LineHeight:F1}px");
|
||||
$"lineHeight={_debugFont.LineHeight:F1}px (reserved for D.6 HUD)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("debug overlay: no system monospace font found; HUD disabled");
|
||||
Console.WriteLine("world-hud font: no system monospace font found");
|
||||
}
|
||||
|
||||
var orbit = new OrbitCamera { Aspect = _window!.Size.X / (float)_window.Size.Y };
|
||||
|
|
@ -959,7 +921,50 @@ public sealed class GameWindow : IDisposable
|
|||
_panelHost.Register(
|
||||
new AcDream.UI.Abstractions.Panels.Chat.ChatPanel(chatVm));
|
||||
|
||||
Console.WriteLine("devtools: ImGui panel host ready (VitalsPanel + ChatPanel registered)");
|
||||
// Phase I.2: DebugPanel — replaces the deleted custom
|
||||
// DebugOverlay (six floating panels + hint bar + toast).
|
||||
// The VM closes over every data source the old snapshot
|
||||
// record exposed; reads are live (no per-frame snapshot
|
||||
// build). Action hooks tie the panel's cycle/toggle
|
||||
// buttons back to the same routines the F2/F7/F10
|
||||
// keybinds use.
|
||||
_debugVm = new AcDream.UI.Abstractions.Panels.Debug.DebugVM(
|
||||
getPlayerPosition: () => GetDebugPlayerPosition(),
|
||||
getPlayerHeadingDeg: () => GetDebugPlayerHeadingDeg(),
|
||||
getPlayerCellId: () => GetDebugPlayerCellId(),
|
||||
getPlayerOnGround: () => GetDebugPlayerOnGround(),
|
||||
getInPlayerMode: () => _playerMode,
|
||||
getInFlyMode: () => _cameraController?.IsFlyMode ?? false,
|
||||
getVerticalVelocity: () => _playerController?.VerticalVelocity ?? 0f,
|
||||
getEntityCount: () => _worldState.Entities.Count,
|
||||
getAnimatedCount: () => _animatedEntities.Count,
|
||||
getLandblocksVisible: () => _lastVisibleLandblocks,
|
||||
getLandblocksTotal: () => _lastTotalLandblocks,
|
||||
getShadowObjectCount: () => _physicsEngine.ShadowObjects.TotalRegistered,
|
||||
getNearestObjDist: () => _lastNearestObjDist,
|
||||
getNearestObjLabel: () => _lastNearestObjLabel,
|
||||
getColliding: () => _lastColliding,
|
||||
getDebugWireframes: () => _debugCollisionVisible,
|
||||
getStreamingRadius: () => _streamingRadius,
|
||||
getMouseSensitivity: () => GetActiveSensitivity(),
|
||||
getChaseDistance: () => _chaseCamera?.Distance ?? 0f,
|
||||
getRmbOrbit: () => _rmbHeld,
|
||||
getHourName: () => WorldTime.CurrentCalendar.Hour.ToString(),
|
||||
getDayFraction: () => (float)WorldTime.DayFraction,
|
||||
getWeather: () => Weather.Kind.ToString(),
|
||||
getActiveLights: () => Lighting.ActiveCount,
|
||||
getRegisteredLights: () => Lighting.RegisteredCount,
|
||||
getParticleCount: () => _particleSystem?.ActiveParticleCount ?? 0,
|
||||
getFps: () => (float)_lastFps,
|
||||
getFrameMs: () => (float)_lastFrameMs,
|
||||
combat: Combat);
|
||||
_debugVm.CycleTimeOfDay = CycleTimeOfDay;
|
||||
_debugVm.CycleWeather = CycleWeather;
|
||||
_debugVm.ToggleCollisionWires = ToggleCollisionWires;
|
||||
_debugPanel = new AcDream.UI.Abstractions.Panels.Debug.DebugPanel(_debugVm);
|
||||
_panelHost.Register(_debugPanel);
|
||||
|
||||
Console.WriteLine("devtools: ImGui panel host ready (VitalsPanel + ChatPanel + DebugPanel registered)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -968,6 +973,8 @@ public sealed class GameWindow : IDisposable
|
|||
_imguiBootstrap = null;
|
||||
_panelHost = null;
|
||||
_vitalsVm = null;
|
||||
_debugVm = null;
|
||||
_debugPanel = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4119,48 +4126,32 @@ public sealed class GameWindow : IDisposable
|
|||
visibleLandblocks++;
|
||||
}
|
||||
|
||||
// ── Debug HUD overlay ────────────────────────────────────────────
|
||||
// Build a per-frame snapshot of state we want to show and hand it
|
||||
// to the overlay. Drawn after all 3D passes so it sits on top.
|
||||
if (_debugOverlay is not null && _textRenderer is not null && _debugFont is not null)
|
||||
// Phase I.2: refresh per-frame fields that DebugVM closures
|
||||
// can't compute lazily (frustum-derived counters + nearest-
|
||||
// object scan). Every other DebugVM field reads through to
|
||||
// the live source via its closure. Skipped entirely when
|
||||
// devtools are off — avoids the nearest-object O(N) scan in
|
||||
// the hot path of an offline render.
|
||||
if (_debugVm is not null)
|
||||
{
|
||||
System.Numerics.Vector3 playerPos;
|
||||
float headingDeg;
|
||||
uint cellId;
|
||||
bool onGround;
|
||||
float vVel;
|
||||
if (_playerMode && _playerController is not null)
|
||||
{
|
||||
playerPos = _playerController.Position;
|
||||
// Yaw in math convention: 0 = +X east, PI/2 = +Y north.
|
||||
// Convert to degrees in [0, 360).
|
||||
headingDeg = _playerController.Yaw * (180f / MathF.PI);
|
||||
headingDeg %= 360f;
|
||||
if (headingDeg < 0f) headingDeg += 360f;
|
||||
cellId = _playerController.CellId;
|
||||
onGround = !_playerController.IsAirborne;
|
||||
vVel = _playerController.VerticalVelocity;
|
||||
}
|
||||
else
|
||||
{
|
||||
playerPos = camPos;
|
||||
var camFwd = new System.Numerics.Vector3(-invView.M31, -invView.M32, -invView.M33);
|
||||
headingDeg = MathF.Atan2(camFwd.Y, camFwd.X) * (180f / MathF.PI);
|
||||
if (headingDeg < 0f) headingDeg += 360f;
|
||||
cellId = 0u;
|
||||
onGround = false;
|
||||
vVel = 0f;
|
||||
}
|
||||
_lastVisibleLandblocks = visibleLandblocks;
|
||||
_lastTotalLandblocks = totalLandblocks;
|
||||
|
||||
// Compute fly/orbit-mode camera position for the nearest-
|
||||
// object scan when not in player mode.
|
||||
System.Numerics.Vector3 nearOrigin;
|
||||
if (_playerMode && _playerController is not null)
|
||||
nearOrigin = _playerController.Position;
|
||||
else
|
||||
nearOrigin = camPos;
|
||||
|
||||
// Nearest shadow object — surface-to-surface distance in XY
|
||||
// (subtract player radius + obj radius). Negative == penetrating.
|
||||
const float playerRadius = 0.48f;
|
||||
float bestDist = float.PositiveInfinity;
|
||||
string bestLabel = "-";
|
||||
foreach (var obj in _physicsEngine.ShadowObjects.AllEntriesForDebug())
|
||||
{
|
||||
float dx = obj.Position.X - playerPos.X;
|
||||
float dy = obj.Position.Y - playerPos.Y;
|
||||
float dx = obj.Position.X - nearOrigin.X;
|
||||
float dy = obj.Position.Y - nearOrigin.Y;
|
||||
float d = MathF.Sqrt(dx * dx + dy * dy) - obj.Radius - playerRadius;
|
||||
if (d < bestDist)
|
||||
{
|
||||
|
|
@ -4168,53 +4159,9 @@ public sealed class GameWindow : IDisposable
|
|||
bestLabel = $"0x{obj.EntityId:X8} {obj.CollisionType}";
|
||||
}
|
||||
}
|
||||
bool colliding = bestDist < 0.05f;
|
||||
if (bestDist < 0f) bestDist = 0f;
|
||||
|
||||
// Select the active-mode sensitivity to display.
|
||||
float activeSens;
|
||||
if (_playerMode && _cameraController?.IsChaseMode == true)
|
||||
activeSens = _sensChase;
|
||||
else if (_cameraController?.IsFlyMode == true)
|
||||
activeSens = _sensFly;
|
||||
else
|
||||
activeSens = _sensOrbit;
|
||||
|
||||
// Phase G: pull sky + weather + lighting state for the overlay.
|
||||
var dayCal = WorldTime.CurrentCalendar;
|
||||
var snapshot = new DebugOverlay.Snapshot(
|
||||
Fps: (float)_lastFps,
|
||||
FrameTimeMs: (float)_lastFrameMs,
|
||||
PlayerPos: playerPos,
|
||||
HeadingDeg: headingDeg,
|
||||
CellId: cellId,
|
||||
OnGround: onGround,
|
||||
InPlayerMode: _playerMode,
|
||||
InFlyMode: _cameraController?.IsFlyMode ?? false,
|
||||
VerticalVelocity: vVel,
|
||||
EntityCount: _worldState.Entities.Count,
|
||||
AnimatedCount: _animatedEntities.Count,
|
||||
LandblocksVisible: visibleLandblocks,
|
||||
LandblocksTotal: totalLandblocks,
|
||||
ShadowObjectCount: _physicsEngine.ShadowObjects.TotalRegistered,
|
||||
NearestObjDist: bestDist,
|
||||
NearestObjLabel: bestLabel,
|
||||
Colliding: colliding,
|
||||
DebugWireframes: _debugCollisionVisible,
|
||||
StreamingRadius: _streamingRadius,
|
||||
MouseSensitivity: activeSens,
|
||||
ChaseDistance: _chaseCamera?.Distance ?? 0f,
|
||||
RmbOrbit: _rmbHeld,
|
||||
HourName: dayCal.Hour.ToString(),
|
||||
DayFraction: (float)WorldTime.DayFraction,
|
||||
Weather: Weather.Kind.ToString(),
|
||||
ActiveLights: Lighting.ActiveCount,
|
||||
RegisteredLights: Lighting.RegisteredCount,
|
||||
ParticleCount: _particleSystem?.ActiveParticleCount ?? 0);
|
||||
|
||||
_debugOverlay.Update((float)deltaSeconds);
|
||||
var size = new System.Numerics.Vector2(_window!.Size.X, _window.Size.Y);
|
||||
_debugOverlay.Draw(snapshot, size);
|
||||
_lastColliding = bestDist < 0.05f;
|
||||
_lastNearestObjDist = bestDist < 0f ? 0f : bestDist;
|
||||
_lastNearestObjLabel = bestLabel;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4939,6 +4886,146 @@ public sealed class GameWindow : IDisposable
|
|||
EndSize = 0.06f,
|
||||
};
|
||||
|
||||
// ── Phase I.2 — DebugPanel helpers ────────────────────────────────
|
||||
//
|
||||
// The ImGui DebugPanel reads through DebugVM closures that ask
|
||||
// GameWindow for live state on every frame. The helper methods below
|
||||
// are the *named* targets of those closures (and of the F-key
|
||||
// shortcuts that share the same actions). Keeping them as methods
|
||||
// (vs ad-hoc lambdas where the VM is constructed) means both the
|
||||
// panel button and the keybind run the *same* code, so behavior
|
||||
// can't drift between the two surfaces.
|
||||
|
||||
/// <summary>Player-mode-aware position source for the DebugPanel.</summary>
|
||||
private System.Numerics.Vector3 GetDebugPlayerPosition()
|
||||
{
|
||||
if (_playerMode && _playerController is not null)
|
||||
return _playerController.Position;
|
||||
if (_cameraController?.Active is { } cam)
|
||||
{
|
||||
// Camera world position from inverse of view matrix — same
|
||||
// computation used by the scene-lighting UBO each frame.
|
||||
System.Numerics.Matrix4x4.Invert(cam.View, out var inv);
|
||||
return new System.Numerics.Vector3(inv.M41, inv.M42, inv.M43);
|
||||
}
|
||||
return System.Numerics.Vector3.Zero;
|
||||
}
|
||||
|
||||
/// <summary>Heading in degrees, [0..360). Player yaw in player mode, camera-forward heading otherwise.</summary>
|
||||
private float GetDebugPlayerHeadingDeg()
|
||||
{
|
||||
float deg;
|
||||
if (_playerMode && _playerController is not null)
|
||||
{
|
||||
deg = _playerController.Yaw * (180f / MathF.PI);
|
||||
}
|
||||
else if (_cameraController?.Active is { } cam)
|
||||
{
|
||||
// Camera-relative heading from view matrix forward vector. Use
|
||||
// the same -invView.Mxx convention the snapshot block used.
|
||||
System.Numerics.Matrix4x4.Invert(cam.View, out var inv);
|
||||
var fwd = new System.Numerics.Vector3(-inv.M31, -inv.M32, -inv.M33);
|
||||
deg = MathF.Atan2(fwd.Y, fwd.X) * (180f / MathF.PI);
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
deg %= 360f;
|
||||
if (deg < 0f) deg += 360f;
|
||||
return deg;
|
||||
}
|
||||
|
||||
private uint GetDebugPlayerCellId() =>
|
||||
_playerMode && _playerController is not null ? _playerController.CellId : 0u;
|
||||
|
||||
private bool GetDebugPlayerOnGround() =>
|
||||
_playerMode && _playerController is not null && !_playerController.IsAirborne;
|
||||
|
||||
private float GetActiveSensitivity()
|
||||
{
|
||||
if (_playerMode && _cameraController?.IsChaseMode == true) return _sensChase;
|
||||
if (_cameraController?.IsFlyMode == true) return _sensFly;
|
||||
return _sensOrbit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cycle the time-of-day debug override. Same body as the old F7
|
||||
/// keybind handler; called by both the keybind AND the DebugPanel
|
||||
/// "Cycle time of day" button via DebugVM.CycleTimeOfDay.
|
||||
/// </summary>
|
||||
private void CycleTimeOfDay()
|
||||
{
|
||||
// none → 0.0 (midnight) → 0.25 (dawn) → 0.5 (noon) → 0.75 (dusk) → none
|
||||
_timeDebugStep = (_timeDebugStep + 1) % 5;
|
||||
float? pick = _timeDebugStep switch
|
||||
{
|
||||
0 => (float?)null,
|
||||
1 => 0.0f,
|
||||
2 => 0.25f,
|
||||
3 => 0.5f,
|
||||
4 => 0.75f,
|
||||
_ => null,
|
||||
};
|
||||
if (pick.HasValue)
|
||||
{
|
||||
WorldTime.SetDebugTime(pick.Value);
|
||||
_debugVm?.AddToast($"Time override = {pick.Value:F2}");
|
||||
}
|
||||
else
|
||||
{
|
||||
WorldTime.ClearDebugTime();
|
||||
_debugVm?.AddToast("Time override cleared");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cycle the weather kind. Same body as the old F10 keybind handler.
|
||||
/// </summary>
|
||||
private void CycleWeather()
|
||||
{
|
||||
var kinds = new[]
|
||||
{
|
||||
AcDream.Core.World.WeatherKind.Clear,
|
||||
AcDream.Core.World.WeatherKind.Overcast,
|
||||
AcDream.Core.World.WeatherKind.Rain,
|
||||
AcDream.Core.World.WeatherKind.Snow,
|
||||
AcDream.Core.World.WeatherKind.Storm,
|
||||
};
|
||||
_weatherDebugStep = (_weatherDebugStep + 1) % kinds.Length;
|
||||
Weather.ForceWeather(kinds[_weatherDebugStep]);
|
||||
_debugVm?.AddToast($"Weather = {kinds[_weatherDebugStep]}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle the collision-wires debug renderer. Same body as the old
|
||||
/// F2 keybind handler.
|
||||
/// </summary>
|
||||
private void ToggleCollisionWires()
|
||||
{
|
||||
_debugCollisionVisible = !_debugCollisionVisible;
|
||||
_debugVm?.AddToast($"Collision wireframes {(_debugCollisionVisible ? "ON" : "OFF")}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Yields the registered DebugPanel(s) so F1 can flip their
|
||||
/// visibility. Returns nothing when devtools are off.
|
||||
/// </summary>
|
||||
private IEnumerable<AcDream.UI.Abstractions.IPanel> EnumerateDebugPanel()
|
||||
{
|
||||
// The current ImGuiPanelHost only exposes Register/Unregister,
|
||||
// not enumerate. We track the DebugPanel through the VM presence
|
||||
// — the panel is registered iff _debugVm is non-null. Look it
|
||||
// up via the panel ID convention.
|
||||
// Defer the actual lookup to the panel host once it grows an
|
||||
// accessor; for now, no-op when devtools are off.
|
||||
if (_debugPanel is not null) yield return _debugPanel;
|
||||
}
|
||||
|
||||
// Cached panel reference so EnumerateDebugPanel can return it. Set
|
||||
// in the DevToolsEnabled construction block above; null otherwise.
|
||||
private AcDream.UI.Abstractions.Panels.Debug.DebugPanel? _debugPanel;
|
||||
|
||||
private void OnClosing()
|
||||
{
|
||||
// Phase A.1: join the streamer worker thread before tearing down GL
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue