fix #213: complete suicide and framerate presentation

Translate suicide response 0x004A as retail's successful self-kill notice. Port /framerate onto the authored SmartBox FPS element with live two-decimal FPS and DEG values, keep diagnostic window chrome independent, and synchronize command-driven settings without discarding panel drafts.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-13 15:50:47 +02:00
parent 9ea579bdd0
commit 43f7c7807c
17 changed files with 729 additions and 39 deletions

View file

@ -2150,6 +2150,15 @@ public sealed class GameWindow : IDisposable
SetRetailCombatGameplay),
JumpPowerbar: new AcDream.App.UI.JumpPowerbarRuntimeBindings(
() => _playerController?.JumpCharge ?? default),
Fps: new AcDream.App.UI.FpsRuntimeBindings(
() => _lastFps,
// Retail displays either the user degrade bias or the live
// distance-driven multiplier. acdream does not yet implement
// adaptive distance degradation (tracked by TS-15), so its
// effective multiplier is exactly 1.0.
() => 1.0,
() => _settingsVm?.DisplayDraft.ShowFps
?? _persistedDisplay.ShowFps),
Toolbar: new AcDream.App.UI.ToolbarRuntimeBindings(
Objects,
() => Shortcuts,
@ -9988,32 +9997,23 @@ public sealed class GameWindow : IDisposable
int entityCount = _worldState.Entities.Count;
int animatedCount = _animatedEntities.Count;
// L.0 Display tab: ShowFps gates the perf string in the
// title bar. Default is true (matches pre-L.0 behaviour);
// unchecking the toggle in Display tab collapses the title
// to just "acdream" for a cleaner alt-tab experience.
// Keep the developer diagnostic title independent from retail's
// /framerate switch. That command owns the in-world SmartBox readout;
// conflating it with window chrome was the original incomplete port.
//
// When perf is shown, also include the in-game calendar/time —
// Also include the in-game calendar/time —
// matches retail's @timestamp output ("Date: <Month> <Day>,
// PY <Year> Time: <HourName>"). Uses NowTicks (server-synced
// + wall-clock interpolation) so the user can read the same
// fields off both acdream and retail and confirm clock parity
// directly. Drift > 1 hour = real bug.
bool showFps = _settingsVm?.DisplayDraft.ShowFps ?? true;
if (showFps)
{
double tNow = WorldTime.NowTicks;
var titleCal = AcDream.Core.World.DerethDateTime.ToCalendar(tNow);
double df = WorldTime.DayFraction;
_window!.Title =
$"acdream | {fps:F0} fps | {avgFrameTime:F1} ms | "
+ $"lb {visibleLandblocks}/{totalLandblocks} | ent {entityCount}/anim {animatedCount} | "
+ $"PY{titleCal.Year} {titleCal.Month} {titleCal.Day} {titleCal.Hour} (df={df:F4})";
}
else
{
_window!.Title = "acdream";
}
double tNow = WorldTime.NowTicks;
var titleCal = AcDream.Core.World.DerethDateTime.ToCalendar(tNow);
double df = WorldTime.DayFraction;
_window!.Title =
$"acdream | {fps:F0} fps | {avgFrameTime:F1} ms | "
+ $"lb {visibleLandblocks}/{totalLandblocks} | ent {entityCount}/anim {animatedCount} | "
+ $"PY{titleCal.Year} {titleCal.Month} {titleCal.Day} {titleCal.Hour} (df={df:F4})";
_lastFps = fps;
_lastFrameMs = avgFrameTime;
_perfAccum = 0;
@ -11482,7 +11482,7 @@ public sealed class GameWindow : IDisposable
ShowFps = !_persistedDisplay.ShowFps,
};
if (_settingsVm is not null)
_settingsVm.SetDisplay(_settingsVm.DisplayDraft with
_settingsVm.ApplyExternalDisplayChange(display => display with
{
ShowFps = _persistedDisplay.ShowFps,
});

View file

@ -0,0 +1,79 @@
using System.Globalization;
namespace AcDream.App.UI.Layout;
/// <summary>
/// Binds retail's SmartBox frame-rate display to the authored text element from
/// <c>LayoutDesc 0x2100000F</c>.
/// </summary>
/// <remarks>
/// Faithful port of <c>gmSmartBoxUI::RecvNotice_SetFramerateDisplay @ 0x004D65E0</c>,
/// <c>gmSmartBoxUI::UpdateFPSMeter @ 0x004D63A0</c>, and
/// <c>gmSmartBoxUI::UseTime @ 0x004D6E30</c>. Portal string table 0x23000001,
/// string 0x0DCFFF73 supplies the two labels <c>FPS:</c> and <c>DEG:</c>;
/// retail inserts both floating-point values with two decimal places.
/// </remarks>
public sealed class RetailFpsController
{
public const uint LayoutId = 0x2100000Fu;
public const uint DisplayElementId = 0x10000047u;
private readonly UiText _display;
private readonly Func<double> _framesPerSecond;
private readonly Func<double> _degradeMultiplier;
private readonly Func<bool> _isVisible;
private RetailFpsController(
UiText display,
Func<double> framesPerSecond,
Func<double> degradeMultiplier,
Func<bool> isVisible)
{
_display = display;
_framesPerSecond = framesPerSecond;
_degradeMultiplier = degradeMultiplier;
_isVisible = isVisible;
// The selected catalog element is authored at (1,1) inside gmSmartBoxUI.
// Selective LayoutDesc import makes it a root, so restore that parent-relative
// placement explicitly instead of letting it drift to (0,0).
_display.Left = 1f;
_display.Top = 1f;
_display.Padding = 1f;
_display.Selectable = false;
_display.LinesProvider = BuildLines;
Tick();
}
public UiText Display => _display;
public static RetailFpsController? Bind(
ImportedLayout layout,
Func<double> framesPerSecond,
Func<double> degradeMultiplier,
Func<bool> isVisible)
{
ArgumentNullException.ThrowIfNull(layout);
ArgumentNullException.ThrowIfNull(framesPerSecond);
ArgumentNullException.ThrowIfNull(degradeMultiplier);
ArgumentNullException.ThrowIfNull(isVisible);
return layout.FindElement(DisplayElementId) is UiText display
? new RetailFpsController(display, framesPerSecond, degradeMultiplier, isVisible)
: null;
}
/// <summary>Mirrors the retail notice-driven show/hide state each UI tick.</summary>
public void Tick() => _display.Visible = _isVisible();
private IReadOnlyList<UiText.Line> BuildLines()
{
string fps = _framesPerSecond().ToString("F2", CultureInfo.InvariantCulture);
string degrade = _degradeMultiplier().ToString("F2", CultureInfo.InvariantCulture);
return
[
new UiText.Line($"FPS: {fps}", _display.DefaultColor),
new UiText.Line($"DEG: {degrade}", _display.DefaultColor),
];
}
}

View file

@ -45,6 +45,11 @@ public sealed record CombatRuntimeBindings(
public sealed record JumpPowerbarRuntimeBindings(Func<JumpChargeSnapshot> Snapshot);
public sealed record FpsRuntimeBindings(
Func<double> FramesPerSecond,
Func<double> DegradeMultiplier,
Func<bool> IsVisible);
public sealed record ToolbarRuntimeBindings(
ClientObjectTable Objects,
Func<IReadOnlyList<ShortcutEntry>> Shortcuts,
@ -110,6 +115,7 @@ public sealed record RetailUiRuntimeBindings(
RadarRuntimeBindings Radar,
CombatRuntimeBindings Combat,
JumpPowerbarRuntimeBindings JumpPowerbar,
FpsRuntimeBindings Fps,
ToolbarRuntimeBindings Toolbar,
CharacterRuntimeBindings Character,
InventoryRuntimeBindings Inventory,
@ -135,6 +141,7 @@ public sealed class RetailUiRuntime : IDisposable
private RetailUiRuntime(RetailUiRuntimeBindings bindings)
{
_bindings = bindings;
MountFpsDisplay();
MountVitals();
MountRadar();
MountChat();
@ -180,6 +187,7 @@ public sealed class RetailUiRuntime : IDisposable
public ToolbarInputController? ToolbarInputController { get; private set; }
public CombatUiController? CombatUiController { get; private set; }
public JumpPowerbarController? JumpPowerbarController { get; private set; }
public RetailFpsController? FpsController { get; private set; }
public SelectedObjectController? SelectedObjectController { get; private set; }
public UiViewport? PaperdollViewportWidget { get; private set; }
public UiNineSlicePanel? InventoryFrame { get; private set; }
@ -201,6 +209,7 @@ public sealed class RetailUiRuntime : IDisposable
public void Tick(double deltaSeconds)
{
FpsController?.Tick();
JumpPowerbarController?.Tick();
SelectedObjectController?.Tick(deltaSeconds);
ConfirmationDialogs?.Tick();
@ -269,6 +278,47 @@ public sealed class RetailUiRuntime : IDisposable
_bindings.Assets.ResolveFont);
}
private ImportedLayout? Import(uint layoutId, uint rootElementId)
{
lock (_bindings.Assets.DatLock)
return LayoutImporter.Import(
_bindings.Assets.Dats,
layoutId,
rootElementId,
_bindings.Assets.ResolveSprite,
_bindings.Assets.DefaultFont,
_bindings.Assets.ResolveFont);
}
private void MountFpsDisplay()
{
ImportedLayout? layout = Import(
RetailFpsController.LayoutId,
RetailFpsController.DisplayElementId);
if (layout is null)
{
Console.WriteLine("[D.2b] FPS display: SmartBox element 0x10000047 not found.");
return;
}
FpsRuntimeBindings b = _bindings.Fps;
FpsController = RetailFpsController.Bind(
layout,
b.FramesPerSecond,
b.DegradeMultiplier,
b.IsVisible);
if (FpsController is null)
{
Console.WriteLine("[D.2b] FPS display: SmartBox element is not UIElement_Text.");
return;
}
// SmartBox HUD content is mounted before movable windows, matching retail's
// screen-layer ordering and ensuring ordinary panels can cover the readout.
Host.Root.AddChild(layout.Root);
Console.WriteLine("[D.2b] retail FPS display from SmartBox LayoutDesc 0x2100000F.");
}
private void MountVitals()
{
ImportedLayout? layout = Import(0x2100006Cu);