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:
parent
9ea579bdd0
commit
43f7c7807c
17 changed files with 729 additions and 39 deletions
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
79
src/AcDream.App/UI/Layout/RetailFpsController.cs
Normal file
79
src/AcDream.App/UI/Layout/RetailFpsController.cs
Normal 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),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ public static class WeenieErrorMessages
|
|||
// Movement / teleport / housing
|
||||
[0x0036] = "Action cancelled!", // ActionCancelled
|
||||
[0x003D] = "You charged too far!", // YouChargedTooFar
|
||||
[0x004A] = "Ack! You killed yourself!", // YouKilledYourself
|
||||
[0x0498] = "You have moved too far!", // YouHaveMovedTooFar
|
||||
[0x0499] = "That is not a valid destination!", // TeleToInvalidPosition
|
||||
[0x0532] = "You must wait 30 days after purchasing a house before you may purchase another with any character on the same account.",
|
||||
|
|
|
|||
|
|
@ -25,19 +25,17 @@ public sealed record DisplaySettings(
|
|||
QualityPreset Quality)
|
||||
{
|
||||
/// <summary>Values used on first launch / when settings.json is absent.
|
||||
/// All defaults pinned to the pre-L.0 runtime state — Resolution
|
||||
/// Geometry/render defaults preserve the pre-L.0 runtime state — Resolution
|
||||
/// matches the WindowOptions startup size (1280×720), FieldOfView
|
||||
/// matches camera FovY (60°), VSync matches WindowOptions (false),
|
||||
/// ShowFps preserves the perf string in the title bar. Net effect:
|
||||
/// opening Display + Save without touching anything is a complete
|
||||
/// visual no-op.</summary>
|
||||
/// ShowFps matches retail's initially-hidden SmartBox FPS readout.</summary>
|
||||
public static DisplaySettings Default { get; } = new(
|
||||
Resolution: "1280x720",
|
||||
Fullscreen: false,
|
||||
VSync: false,
|
||||
FieldOfView: 60f,
|
||||
Gamma: 1.0f,
|
||||
ShowFps: true,
|
||||
ShowFps: false,
|
||||
Quality: QualityPreset.High);
|
||||
|
||||
/// <summary>16:9 resolution presets offered in the dropdown.</summary>
|
||||
|
|
|
|||
|
|
@ -151,6 +151,20 @@ public sealed class SettingsVM
|
|||
_displayDraft = value ?? throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply one externally persisted display-option change to both snapshots.
|
||||
/// Existing unsaved edits to other fields remain drafts rather than being
|
||||
/// accidentally promoted to persisted state.
|
||||
/// </summary>
|
||||
public void ApplyExternalDisplayChange(Func<DisplaySettings, DisplaySettings> update)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(update);
|
||||
_displayPersisted = update(_displayPersisted)
|
||||
?? throw new InvalidOperationException("Display update returned null.");
|
||||
_displayDraft = update(_displayDraft)
|
||||
?? throw new InvalidOperationException("Display update returned null.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replace the entire Audio draft with <paramref name="value"/>.
|
||||
/// Live audio preview is achieved at the host layer by pushing
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue