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
250
src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs
Normal file
250
src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
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 =
|
||||
{
|
||||
("Esc", "exit fly / player / close window"),
|
||||
("Tab", "toggle player mode (when in-world)"),
|
||||
("F", "toggle fly camera"),
|
||||
("F1", "toggle this debug panel"),
|
||||
("F2", "toggle collision wireframes"),
|
||||
("F3", "console dump (pos + nearby objects)"),
|
||||
("F7", "cycle time-of-day override"),
|
||||
("F8 / F9", "mouse sensitivity slower / faster"),
|
||||
("F10", "cycle weather"),
|
||||
("W A S D", "move (player mode) / fly"),
|
||||
("Mouse", "turn character / look (fly)"),
|
||||
("Hold RMB", "free orbit camera around player"),
|
||||
("Wheel", "zoom chase camera in / out"),
|
||||
("Space", "jump"),
|
||||
("Shift", "run"),
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Render(PanelContext ctx, IPanelRenderer renderer)
|
||||
{
|
||||
if (!renderer.Begin(Title))
|
||||
{
|
||||
renderer.End();
|
||||
return;
|
||||
}
|
||||
|
||||
DrawPlayerInfo(renderer);
|
||||
DrawPerformance(renderer);
|
||||
DrawCompass(renderer);
|
||||
DrawHelp(renderer);
|
||||
DrawCombatEvents(renderer);
|
||||
DrawRecentToasts(renderer);
|
||||
DrawDiagnostics(renderer);
|
||||
|
||||
renderer.End();
|
||||
}
|
||||
|
||||
// ── Sections ──────────────────────────────────────────────────────
|
||||
|
||||
private void DrawPlayerInfo(IPanelRenderer r)
|
||||
{
|
||||
if (!r.CollapsingHeader("Player Info", defaultOpen: true)) return;
|
||||
|
||||
string mode = _vm.InPlayerMode ? "PLAYER"
|
||||
: _vm.InFlyMode ? "FLY"
|
||||
: "ORBIT";
|
||||
r.Text($"mode: {mode} cell: 0x{_vm.CellId:X8}");
|
||||
var p = _vm.PlayerPosition;
|
||||
r.Text($"pos: ({p.X,7:F1}, {p.Y,7:F1}, {p.Z,7:F2})");
|
||||
r.Text($"heading: {_vm.HeadingDeg,3:F0}°");
|
||||
r.Text($"grounded: {(_vm.OnGround ? "yes" : "no ")} vZ: {_vm.VerticalVelocity,5:F2}");
|
||||
|
||||
string near = float.IsPositiveInfinity(_vm.NearestObjDist)
|
||||
? "---"
|
||||
: $"{_vm.NearestObjDist,4:F1}m";
|
||||
if (_vm.Colliding)
|
||||
{
|
||||
r.TextColored(new Vector4(1f, 0.4f, 0.35f, 1f),
|
||||
$"near: {near} {_vm.NearestObjLabel} [BLOCKED]");
|
||||
}
|
||||
else
|
||||
{
|
||||
r.Text($"near: {near} {_vm.NearestObjLabel}");
|
||||
}
|
||||
|
||||
if (_vm.InPlayerMode)
|
||||
r.Text($"chase dist: {_vm.ChaseDistance,4:F1}m{(_vm.RmbOrbit ? " [RMB orbit]" : "")}");
|
||||
r.Text($"sens: {_vm.MouseSensitivity:F3}x");
|
||||
}
|
||||
|
||||
private void DrawPerformance(IPanelRenderer r)
|
||||
{
|
||||
if (!r.CollapsingHeader("Performance", defaultOpen: true)) return;
|
||||
|
||||
r.Text($"fps: {_vm.Fps,5:F0} frame: {_vm.FrameMs,5:F1} ms");
|
||||
r.Text($"visible LB: {_vm.LandblocksVisible,3}/{_vm.LandblocksTotal,3} radius: {_vm.StreamingRadius}");
|
||||
r.Text($"entities: {_vm.EntityCount,4} animated: {_vm.AnimatedCount,3} coll: {_vm.ShadowObjectCount}");
|
||||
r.Text($"lights: {_vm.ActiveLights}/{_vm.RegisteredLights} particles: {_vm.ParticleCount}");
|
||||
r.Text($"time: {_vm.DayFraction,5:F2} {_vm.HourName} weather: {_vm.Weather}");
|
||||
}
|
||||
|
||||
private void DrawCompass(IPanelRenderer r)
|
||||
{
|
||||
if (!r.CollapsingHeader("Compass", defaultOpen: false)) return;
|
||||
|
||||
// Phase I.2 stub — the visual strip + cardinal markers from the
|
||||
// old DebugOverlay relied on raw 2D-rect primitives we don't (and
|
||||
// shouldn't) expose through IPanelRenderer. The fancy compass
|
||||
// strip lands in D.6 with proper world-HUD draw-list primitives.
|
||||
// For now show heading degrees + compass cardinal label.
|
||||
float h = NormalizeDeg(_vm.HeadingDeg);
|
||||
r.Text($"heading: {h,3:F0}° cardinal: {Cardinal(h)}");
|
||||
}
|
||||
|
||||
private void DrawHelp(IPanelRenderer r)
|
||||
{
|
||||
if (!r.CollapsingHeader("Help", defaultOpen: false)) return;
|
||||
|
||||
r.BeginTable("debug.help", 2);
|
||||
foreach (var (key, action) in Keybinds)
|
||||
{
|
||||
r.TableNextColumn();
|
||||
r.Text(key);
|
||||
r.TableNextColumn();
|
||||
r.Text(action);
|
||||
}
|
||||
r.EndTable();
|
||||
}
|
||||
|
||||
private void DrawCombatEvents(IPanelRenderer r)
|
||||
{
|
||||
if (!r.CollapsingHeader("Combat events", defaultOpen: true)) return;
|
||||
|
||||
if (_vm.CombatEvents.Count == 0)
|
||||
{
|
||||
r.Text("(no recent combat)");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var line in _vm.CombatEvents)
|
||||
{
|
||||
r.TextColored(ColorForCombat(line.Kind), line.Text);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawRecentToasts(IPanelRenderer r)
|
||||
{
|
||||
if (!r.CollapsingHeader("Recent toasts", defaultOpen: false)) return;
|
||||
|
||||
if (_vm.RecentToasts.Count == 0)
|
||||
{
|
||||
r.Text("(none)");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var t in _vm.RecentToasts)
|
||||
{
|
||||
string ts = t.Timestamp.ToLocalTime().ToString("HH:mm:ss");
|
||||
r.TextColored(ColorForToast(t.Kind), $"[{ts}] {t.Text}");
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawDiagnostics(IPanelRenderer r)
|
||||
{
|
||||
if (!r.CollapsingHeader("Diagnostics", defaultOpen: true)) return;
|
||||
|
||||
bool dumpMotion = _vm.DumpMotion;
|
||||
bool dumpVitals = _vm.DumpVitals;
|
||||
bool dumpOpcodes = _vm.DumpOpcodes;
|
||||
bool dumpSky = _vm.DumpSky;
|
||||
|
||||
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;
|
||||
|
||||
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();
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
289
src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs
Normal file
289
src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Combat;
|
||||
|
||||
namespace AcDream.UI.Abstractions.Panels.Debug;
|
||||
|
||||
/// <summary>
|
||||
/// Severity tag for a single combat-event line in the
|
||||
/// <see cref="DebugVM.CombatEvents"/> ring. The panel reads this to pick
|
||||
/// a <c>TextColored</c> rgba per row (yellow info / red warn / deep-red
|
||||
/// error). Mirrors the same tri-tone the chat panel uses for combat
|
||||
/// (Phase I.7).
|
||||
/// </summary>
|
||||
public enum CombatEventKind
|
||||
{
|
||||
/// <summary>You dealt damage / landed a hit. Yellow.</summary>
|
||||
Info,
|
||||
/// <summary>An incoming hit you evaded. Red.</summary>
|
||||
Warn,
|
||||
/// <summary>You took damage. Deep red.</summary>
|
||||
Error,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Single typed entry in the combat-events ring. <see cref="Timestamp"/>
|
||||
/// is captured at append time so a future panel revision can fade old
|
||||
/// entries; for I.2 the panel just renders the text + rgba.
|
||||
/// </summary>
|
||||
public readonly record struct CombatEventLine(
|
||||
DateTime Timestamp,
|
||||
CombatEventKind Kind,
|
||||
string Text);
|
||||
|
||||
/// <summary>
|
||||
/// Severity tag for a transient toast message. Mirrors
|
||||
/// <see cref="CombatEventKind"/> but lives in its own enum so the toast
|
||||
/// surface can grow (e.g. an "OK" green) without dragging the combat
|
||||
/// surface along.
|
||||
/// </summary>
|
||||
public enum ToastKind
|
||||
{
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
/// <summary>Single transient toast message kept in the recent-toasts ring.</summary>
|
||||
public readonly record struct ToastMessage(
|
||||
DateTime Timestamp,
|
||||
ToastKind Kind,
|
||||
string Text);
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for the Phase I.2 <see cref="DebugPanel"/>. Read-through
|
||||
/// (no caching): every property forwards to a <c>Func<T></c> that
|
||||
/// the host (<c>GameWindow</c>) wires up at construction. Internal
|
||||
/// state is limited to (a) the combat-event ring buffer, populated via a
|
||||
/// self-subscription to <see cref="CombatState"/>'s typed events
|
||||
/// (replacing the old <c>DebugOverlay.BindCombat</c>); (b) the toast
|
||||
/// ring; (c) the diagnostic-flag bools the panel exposes as checkboxes.
|
||||
///
|
||||
/// <para>
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class DebugVM
|
||||
{
|
||||
/// <summary>Maximum number of combat-event lines kept in the ring.</summary>
|
||||
public const int MaxCombatEvents = 25;
|
||||
|
||||
/// <summary>Maximum number of recent toast messages kept in the ring.</summary>
|
||||
public const int MaxRecentToasts = 25;
|
||||
|
||||
private readonly Func<Vector3> _getPlayerPosition;
|
||||
private readonly Func<float> _getPlayerHeadingDeg;
|
||||
private readonly Func<uint> _getPlayerCellId;
|
||||
private readonly Func<bool> _getPlayerOnGround;
|
||||
private readonly Func<bool> _getInPlayerMode;
|
||||
private readonly Func<bool> _getInFlyMode;
|
||||
private readonly Func<float> _getVerticalVelocity;
|
||||
private readonly Func<int> _getEntityCount;
|
||||
private readonly Func<int> _getAnimatedCount;
|
||||
private readonly Func<int> _getLandblocksVisible;
|
||||
private readonly Func<int> _getLandblocksTotal;
|
||||
private readonly Func<int> _getShadowObjectCount;
|
||||
private readonly Func<float> _getNearestObjDist;
|
||||
private readonly Func<string> _getNearestObjLabel;
|
||||
private readonly Func<bool> _getColliding;
|
||||
private readonly Func<bool> _getDebugWireframes;
|
||||
private readonly Func<int> _getStreamingRadius;
|
||||
private readonly Func<float> _getMouseSensitivity;
|
||||
private readonly Func<float> _getChaseDistance;
|
||||
private readonly Func<bool> _getRmbOrbit;
|
||||
private readonly Func<string> _getHourName;
|
||||
private readonly Func<float> _getDayFraction;
|
||||
private readonly Func<string> _getWeather;
|
||||
private readonly Func<int> _getActiveLights;
|
||||
private readonly Func<int> _getRegisteredLights;
|
||||
private readonly Func<int> _getParticleCount;
|
||||
private readonly Func<float> _getFps;
|
||||
private readonly Func<float> _getFrameMs;
|
||||
|
||||
private readonly Queue<CombatEventLine> _combatEvents = new();
|
||||
private readonly Queue<ToastMessage> _toasts = new();
|
||||
|
||||
/// <summary>
|
||||
/// Build a VM bound to live data sources. Every <c>Func</c> 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.
|
||||
/// </summary>
|
||||
public DebugVM(
|
||||
Func<Vector3> getPlayerPosition,
|
||||
Func<float> getPlayerHeadingDeg,
|
||||
Func<uint> getPlayerCellId,
|
||||
Func<bool> getPlayerOnGround,
|
||||
Func<bool> getInPlayerMode,
|
||||
Func<bool> getInFlyMode,
|
||||
Func<float> getVerticalVelocity,
|
||||
Func<int> getEntityCount,
|
||||
Func<int> getAnimatedCount,
|
||||
Func<int> getLandblocksVisible,
|
||||
Func<int> getLandblocksTotal,
|
||||
Func<int> getShadowObjectCount,
|
||||
Func<float> getNearestObjDist,
|
||||
Func<string> getNearestObjLabel,
|
||||
Func<bool> getColliding,
|
||||
Func<bool> getDebugWireframes,
|
||||
Func<int> getStreamingRadius,
|
||||
Func<float> getMouseSensitivity,
|
||||
Func<float> getChaseDistance,
|
||||
Func<bool> getRmbOrbit,
|
||||
Func<string> getHourName,
|
||||
Func<float> getDayFraction,
|
||||
Func<string> getWeather,
|
||||
Func<int> getActiveLights,
|
||||
Func<int> getRegisteredLights,
|
||||
Func<int> getParticleCount,
|
||||
Func<float> getFps,
|
||||
Func<float> getFrameMs,
|
||||
CombatState combat)
|
||||
{
|
||||
if (combat is null) 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 += d => Push(CombatEventKind.Error,
|
||||
$"<< {d.AttackerName} hit you for {d.Damage}{(d.Critical ? " CRIT!" : "")}");
|
||||
combat.DamageDealtAccepted += d => Push(CombatEventKind.Info,
|
||||
$">> you hit {d.DefenderName} for {d.Damage}");
|
||||
combat.EvadedIncoming += attacker => Push(CombatEventKind.Warn,
|
||||
$"<< {attacker}'s attack missed you");
|
||||
combat.MissedOutgoing += defender => Push(CombatEventKind.Info,
|
||||
$">> your attack missed {defender}");
|
||||
combat.AttackDone += (_, weenieError) =>
|
||||
{
|
||||
if (weenieError != 0)
|
||||
Push(CombatEventKind.Error, $"!! attack failed (error 0x{weenieError:X})");
|
||||
};
|
||||
combat.KillLanded += (victim, _) =>
|
||||
Push(CombatEventKind.Info, $"** you killed {victim}");
|
||||
}
|
||||
|
||||
// ── 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) ───────────────
|
||||
|
||||
/// <summary>Mirror of <c>ACDREAM_DUMP_MOTION</c>; flipped at runtime via the panel.</summary>
|
||||
public bool DumpMotion { get; set; }
|
||||
/// <summary>Mirror of <c>ACDREAM_DUMP_VITALS</c>.</summary>
|
||||
public bool DumpVitals { get; set; }
|
||||
/// <summary>Mirror of <c>ACDREAM_DUMP_OPCODES</c>.</summary>
|
||||
public bool DumpOpcodes { get; set; }
|
||||
/// <summary>Mirror of <c>ACDREAM_DUMP_SKY</c>.</summary>
|
||||
public bool DumpSky { get; set; }
|
||||
|
||||
// ── Action hooks invoked by panel buttons ──────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Cycle the time-of-day debug override (matches the old F7
|
||||
/// behavior — none → midnight → dawn → noon → dusk → none). Wired
|
||||
/// by <c>GameWindow</c>; null when no host is available (tests).
|
||||
/// </summary>
|
||||
public Action? CycleTimeOfDay { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cycle the weather-kind debug override (matches the old F10
|
||||
/// behavior — clear → overcast → rain → snow → storm).
|
||||
/// </summary>
|
||||
public Action? CycleWeather { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Toggle the collision-wires debug renderer. Same effect as the
|
||||
/// old F2 keybind, which we keep as a hotkey alias.
|
||||
/// </summary>
|
||||
public Action? ToggleCollisionWires { get; set; }
|
||||
|
||||
// ── Combat event ring + toast ring ─────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot view of the combat-event ring. Oldest-first; the panel
|
||||
/// can iterate and render each line through <c>TextColored</c>
|
||||
/// based on <see cref="CombatEventLine.Kind"/>.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<CombatEventLine> CombatEvents => _combatEvents;
|
||||
|
||||
/// <summary>Snapshot view of the recent-toasts ring (oldest-first).</summary>
|
||||
public IReadOnlyCollection<ToastMessage> RecentToasts => _toasts;
|
||||
|
||||
/// <summary>
|
||||
/// Append a toast message to the ring. Cap at
|
||||
/// <see cref="MaxRecentToasts"/>; oldest entries drop. The panel's
|
||||
/// "Recent toasts" section reads this; no on-screen flash for I.2.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue