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

@ -107,6 +107,12 @@ public static class FixtureLoader
public static ElementInfo LoadConfirmationDialogInfos()
=> LoadInfos("dialogs_2100003C.json");
public static ImportedLayout LoadFpsDisplay()
=> LayoutImporter.Build(LoadFpsDisplayInfos(), _ => (0u, 0, 0), null);
public static ElementInfo LoadFpsDisplayInfos()
=> LoadInfos("smartbox_fps_2100000F.json");
// ── Shared loader ────────────────────────────────────────────────────────
private static AcDream.App.UI.Layout.ElementInfo LoadInfos(string fileName)

View file

@ -0,0 +1,57 @@
using AcDream.App.UI;
using AcDream.App.UI.Layout;
namespace AcDream.App.Tests.UI.Layout;
public sealed class RetailFpsControllerTests
{
[Fact]
public void AuthoredSmartBoxDisplay_FormatsUpdatesAndTogglesLikeRetail()
{
double fps = 59.994;
double degrade = 0.875;
bool visible = false;
ImportedLayout layout = FixtureLoader.LoadFpsDisplay();
ElementInfo info = FixtureLoader.LoadFpsDisplayInfos();
Assert.Equal(0x4000001Au, info.FontDid);
Assert.Equal(128, info.Width);
Assert.Equal(36, info.Height);
Assert.Equal(1u, info.Left);
Assert.Equal(1u, info.Top);
var controller = RetailFpsController.Bind(
layout, () => fps, () => degrade, () => visible)!;
UiText display = controller.Display;
Assert.Equal(RetailFpsController.DisplayElementId, display.ElementId);
Assert.Equal(128f, display.Width);
Assert.Equal(36f, display.Height);
Assert.Equal(1f, display.Left);
Assert.Equal(1f, display.Top);
Assert.False(display.Visible);
Assert.Equal(
["FPS: 59.99", "DEG: 0.88"],
display.LinesProvider().Select(line => line.Text));
fps = 144.0;
degrade = 1.0;
visible = true;
controller.Tick();
Assert.True(display.Visible);
Assert.Equal(
["FPS: 144.00", "DEG: 1.00"],
display.LinesProvider().Select(line => line.Text));
}
[Fact]
public void Bind_RejectsLayoutWithoutAuthoredTextElement()
{
var root = new UiPanel();
var layout = new ImportedLayout(root, new Dictionary<uint, UiElement>());
Assert.Null(RetailFpsController.Bind(layout, () => 60, () => 1, () => true));
}
}

View file

@ -64,6 +64,25 @@ public sealed class RetailLayoutFixtureGenerator
Path.Combine(FixtureDirectory(), $"dialogs_{dialogsLayoutDid:X8}.json"),
dialogsJson);
const uint smartboxLayoutDid = 0x2100000Fu;
var fps = LayoutImporter.ImportInfos(
dats,
smartboxLayoutDid,
0x10000047u);
Assert.NotNull(fps);
Assert.Equal(0x4000001Au, fps!.FontDid);
var fpsStrings = new DatStringResolver(dats);
Assert.Equal("FPS: ", fpsStrings.Resolve(0x23000001u, 0x0DCFFF73u, 0));
Assert.Equal("\nDEG: ", fpsStrings.Resolve(0x23000001u, 0x0DCFFF73u, 1));
var fpsJson = JsonSerializer.Serialize(fps, new JsonSerializerOptions
{
IncludeFields = true,
WriteIndented = true,
});
File.WriteAllText(
Path.Combine(FixtureDirectory(), $"smartbox_fps_{smartboxLayoutDid:X8}.json"),
fpsJson);
foreach (var (layoutId, fileName) in Layouts)
{
var info = LayoutImporter.ImportInfos(dats, layoutId);

View file

@ -0,0 +1,395 @@
{
"Id": 268435527,
"Type": 12,
"X": 0,
"Y": 0,
"Width": 128,
"Height": 36,
"OriginalParentWidth": 0,
"OriginalParentHeight": 0,
"HasOriginalParentSize": false,
"Left": 1,
"Top": 1,
"Right": 2,
"Bottom": 2,
"ReadOrder": 15,
"ZLevel": 0,
"States": {
"4294967295": {
"Id": 4294967295,
"Name": "",
"PassToChildren": false,
"IncorporationFlags": 30,
"Image": null,
"Cursor": null,
"Properties": {
"Values": {
"33": {
"Kind": 1,
"MasterPropertyId": 33,
"UnsignedValue": 0,
"IntegerValue": 0,
"FloatValue": 0,
"BoolValue": true,
"StringInfoValue": {
"Token": 0,
"StringId": 0,
"TableId": 0,
"Override": 0,
"English": 0,
"Comment": 0
},
"ColorValue": {
"Blue": 0,
"Green": 0,
"Red": 0,
"Alpha": 0
},
"VectorValue": {
"X": 0,
"Y": 0,
"Z": 0
},
"ArrayValue": [],
"StructValue": {}
},
"35": {
"Kind": 4,
"MasterPropertyId": 35,
"UnsignedValue": 0,
"IntegerValue": 2,
"FloatValue": 0,
"BoolValue": false,
"StringInfoValue": {
"Token": 0,
"StringId": 0,
"TableId": 0,
"Override": 0,
"English": 0,
"Comment": 0
},
"ColorValue": {
"Blue": 0,
"Green": 0,
"Red": 0,
"Alpha": 0
},
"VectorValue": {
"X": 0,
"Y": 0,
"Z": 0
},
"ArrayValue": [],
"StructValue": {}
},
"81": {
"Kind": 1,
"MasterPropertyId": 81,
"UnsignedValue": 0,
"IntegerValue": 0,
"FloatValue": 0,
"BoolValue": true,
"StringInfoValue": {
"Token": 0,
"StringId": 0,
"TableId": 0,
"Override": 0,
"English": 0,
"Comment": 0
},
"ColorValue": {
"Blue": 0,
"Green": 0,
"Red": 0,
"Alpha": 0
},
"VectorValue": {
"X": 0,
"Y": 0,
"Z": 0
},
"ArrayValue": [],
"StructValue": {}
},
"59": {
"Kind": 1,
"MasterPropertyId": 59,
"UnsignedValue": 0,
"IntegerValue": 0,
"FloatValue": 0,
"BoolValue": true,
"StringInfoValue": {
"Token": 0,
"StringId": 0,
"TableId": 0,
"Override": 0,
"English": 0,
"Comment": 0
},
"ColorValue": {
"Blue": 0,
"Green": 0,
"Red": 0,
"Alpha": 0
},
"VectorValue": {
"X": 0,
"Y": 0,
"Z": 0
},
"ArrayValue": [],
"StructValue": {}
},
"37": {
"Kind": 4,
"MasterPropertyId": 37,
"UnsignedValue": 0,
"IntegerValue": 2,
"FloatValue": 0,
"BoolValue": false,
"StringInfoValue": {
"Token": 0,
"StringId": 0,
"TableId": 0,
"Override": 0,
"English": 0,
"Comment": 0
},
"ColorValue": {
"Blue": 0,
"Green": 0,
"Red": 0,
"Alpha": 0
},
"VectorValue": {
"X": 0,
"Y": 0,
"Z": 0
},
"ArrayValue": [],
"StructValue": {}
},
"26": {
"Kind": 7,
"MasterPropertyId": 26,
"UnsignedValue": 0,
"IntegerValue": 0,
"FloatValue": 0,
"BoolValue": false,
"StringInfoValue": {
"Token": 0,
"StringId": 0,
"TableId": 0,
"Override": 0,
"English": 0,
"Comment": 0
},
"ColorValue": {
"Blue": 0,
"Green": 0,
"Red": 0,
"Alpha": 0
},
"VectorValue": {
"X": 0,
"Y": 0,
"Z": 0
},
"ArrayValue": [
{
"Kind": 2,
"MasterPropertyId": 24,
"UnsignedValue": 1073741850,
"IntegerValue": 0,
"FloatValue": 0,
"BoolValue": false,
"StringInfoValue": {
"Token": 0,
"StringId": 0,
"TableId": 0,
"Override": 0,
"English": 0,
"Comment": 0
},
"ColorValue": {
"Blue": 0,
"Green": 0,
"Red": 0,
"Alpha": 0
},
"VectorValue": {
"X": 0,
"Y": 0,
"Z": 0
},
"ArrayValue": [],
"StructValue": {}
}
],
"StructValue": {}
},
"27": {
"Kind": 7,
"MasterPropertyId": 27,
"UnsignedValue": 0,
"IntegerValue": 0,
"FloatValue": 0,
"BoolValue": false,
"StringInfoValue": {
"Token": 0,
"StringId": 0,
"TableId": 0,
"Override": 0,
"English": 0,
"Comment": 0
},
"ColorValue": {
"Blue": 0,
"Green": 0,
"Red": 0,
"Alpha": 0
},
"VectorValue": {
"X": 0,
"Y": 0,
"Z": 0
},
"ArrayValue": [
{
"Kind": 6,
"MasterPropertyId": 25,
"UnsignedValue": 0,
"IntegerValue": 0,
"FloatValue": 0,
"BoolValue": false,
"StringInfoValue": {
"Token": 0,
"StringId": 0,
"TableId": 0,
"Override": 0,
"English": 0,
"Comment": 0
},
"ColorValue": {
"Blue": 255,
"Green": 255,
"Red": 255,
"Alpha": 255
},
"VectorValue": {
"X": 0,
"Y": 0,
"Z": 0
},
"ArrayValue": [],
"StructValue": {}
}
],
"StructValue": {}
},
"205": {
"Kind": 0,
"MasterPropertyId": 205,
"UnsignedValue": 3,
"IntegerValue": 0,
"FloatValue": 0,
"BoolValue": false,
"StringInfoValue": {
"Token": 0,
"StringId": 0,
"TableId": 0,
"Override": 0,
"English": 0,
"Comment": 0
},
"ColorValue": {
"Blue": 0,
"Green": 0,
"Red": 0,
"Alpha": 0
},
"VectorValue": {
"X": 0,
"Y": 0,
"Z": 0
},
"ArrayValue": [],
"StructValue": {}
},
"20": {
"Kind": 0,
"MasterPropertyId": 20,
"UnsignedValue": 2,
"IntegerValue": 0,
"FloatValue": 0,
"BoolValue": false,
"StringInfoValue": {
"Token": 0,
"StringId": 0,
"TableId": 0,
"Override": 0,
"English": 0,
"Comment": 0
},
"ColorValue": {
"Blue": 0,
"Green": 0,
"Red": 0,
"Alpha": 0
},
"VectorValue": {
"X": 0,
"Y": 0,
"Z": 0
},
"ArrayValue": [],
"StructValue": {}
},
"21": {
"Kind": 0,
"MasterPropertyId": 21,
"UnsignedValue": 4,
"IntegerValue": 0,
"FloatValue": 0,
"BoolValue": false,
"StringInfoValue": {
"Token": 0,
"StringId": 0,
"TableId": 0,
"Override": 0,
"English": 0,
"Comment": 0
},
"ColorValue": {
"Blue": 0,
"Green": 0,
"Red": 0,
"Alpha": 0
},
"VectorValue": {
"X": 0,
"Y": 0,
"Z": 0
},
"ArrayValue": [],
"StructValue": {}
}
}
}
}
},
"DefaultStateId": 0,
"FontDid": 1073741850,
"HJustify": 0,
"VJustify": 2,
"FontColor": {
"X": 1,
"Y": 1,
"Z": 1,
"W": 1
},
"StateMedia": {},
"StateCursors": {},
"DefaultStateName": "",
"Children": []
}

View file

@ -99,6 +99,7 @@ public sealed class WeenieErrorMessagesTests
[Theory]
[InlineData(0x0036u, "Action cancelled!")]
[InlineData(0x003Du, "You charged too far!")]
[InlineData(0x004Au, "Ack! You killed yourself!")]
public void Format_CombatMovementErrors(uint code, string expected)
=> Assert.Equal(expected, WeenieErrorMessages.Format(code, null));

View file

@ -17,17 +17,14 @@ public sealed class DisplaySettingsTests
// · Resolution matches WindowOptions (1280×720 in GameWindow.Run)
// · FieldOfView matches camera FovY (60° = π/3)
// · VSync matches WindowOptions (false during dev)
// · ShowFps true preserves the perf string in the title bar
// Net effect: opening Display + Save with no edits is a visual
// no-op (no window resize, no camera FovY change, no title
// bar change).
// · ShowFps false matches retail's initially-hidden SmartBox meter
var d = DisplaySettings.Default;
Assert.Equal("1280x720", d.Resolution);
Assert.False(d.Fullscreen);
Assert.False(d.VSync);
Assert.Equal(60f, d.FieldOfView);
Assert.Equal(1.0f, d.Gamma);
Assert.True(d.ShowFps);
Assert.False(d.ShowFps);
}
[Fact]
@ -65,7 +62,7 @@ public sealed class DisplaySettingsTests
// Other fields untouched.
Assert.Equal("1280x720", d.Resolution);
Assert.False(d.VSync);
Assert.True(d.ShowFps);
Assert.False(d.ShowFps);
}
private static int ParseWidth(string res)

View file

@ -271,12 +271,31 @@ public sealed class SettingsVMTests
public void SetDisplay_marks_unsaved_changes()
{
var (vm, _, _, _, _, _, _, _, _, _) = Build();
// Default ShowFps is true → flip to false to ensure the with-
// Default ShowFps is false → flip to true to ensure the with-
// expression actually mutates a field.
vm.SetDisplay(vm.DisplayDraft with { ShowFps = false });
vm.SetDisplay(vm.DisplayDraft with { ShowFps = true });
Assert.True(vm.HasUnsavedChanges);
}
[Fact]
public void ApplyExternalDisplayChange_updates_both_snapshots_and_preserves_other_drafts()
{
var (vm, _, _, _, _, _, _, _, _, _) = Build();
vm.SetDisplay(vm.DisplayDraft with { FieldOfView = 90f });
Assert.True(vm.HasUnsavedChanges);
vm.ApplyExternalDisplayChange(display => display with { ShowFps = true });
Assert.True(vm.DisplayDraft.ShowFps);
Assert.Equal(90f, vm.DisplayDraft.FieldOfView);
Assert.True(vm.HasUnsavedChanges);
vm.Cancel();
Assert.True(vm.DisplayDraft.ShowFps);
Assert.Equal(DisplaySettings.Default.FieldOfView, vm.DisplayDraft.FieldOfView);
}
[Fact]
public void Save_invokes_display_callback_with_draft()
{