From bf77a23ad35b41147de18bd8aa50ed8d60dde302 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 15 Jun 2026 16:30:24 +0200 Subject: [PATCH] feat(D.2b): flip vitals to the LayoutDesc importer; retire hand-authored vitals.xml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The importer (proven pixel-identical at the 2026-06-15 A/B gate) is now the default vitals window when ACDREAM_RETAIL_UI=1 — data-driven from LayoutDesc 0x2100006C. Removed: the hand-authored vitals.xml build path, the asset file (recoverable from git history), and the now-obsolete ACDREAM_RETAIL_UI_IMPORTER flag (RuntimeOptions param + parse + 2 tests). The window is user-positioned at (10,30) and movable; resize stays off — the dat stacked-vitals layout is fixed- size (chrome edges near-pinned), faithful grip/dragbar resize is Plan 2. MarkupDocument/UiNineSlicePanel remain for the chat window + plugin panels. AcDream.App builds 0/0; AcDream.App.Tests 352 passed / 1 skipped / 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 86 ++++++++----------- src/AcDream.App/RuntimeOptions.cs | 2 - src/AcDream.App/UI/assets/vitals.xml | 13 --- .../RuntimeOptionsRetailUiTests.cs | 25 ------ 4 files changed, 37 insertions(+), 89 deletions(-) delete mode 100644 src/AcDream.App/UI/assets/vitals.xml diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 16302f69..c4b55885 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -1782,62 +1782,50 @@ public sealed class GameWindow : IDisposable var controls = _options.AcDir is { } acDir ? AcDream.App.UI.ControlsIni.Load(System.IO.Path.Combine(acDir, "controls", "controls.ini")) : AcDream.App.UI.ControlsIni.Parse(string.Empty); - string vitalsXml = System.IO.File.ReadAllText( - System.IO.Path.Combine(AppContext.BaseDirectory, "UI", "assets", "vitals.xml")); - var panel = AcDream.App.UI.MarkupDocument.Build(vitalsXml, _vitalsVm!, ResolveChrome, controls); - - // Phase D.2b — retail dat-font for the vitals numbers. Font 0x40000000 - // (Latin-1, 16px, outline atlas). The consola TTF debug font is wrong - // for retail look; the meter falls back to it only if the dat font fails - // to load. Loaded under _datLock for consistency with other dat reads - // (no streaming worker is active during OnLoad, but the lock is cheap). + // Phase D.2b — retail dat-font for the vitals numbers (Font 0x40000000, + // Latin-1, 16px, outline atlas). Passed into the importer so the meter + // number overlay renders through the dat-font two-pass blit; falls back to + // the debug font only if it fails to load. Under _datLock like other reads. AcDream.App.UI.UiDatFont? vitalsDatFont; lock (_datLock) vitalsDatFont = AcDream.App.UI.UiDatFont.Load(_dats!, _textureCache!); - if (vitalsDatFont is not null) + Console.WriteLine(vitalsDatFont is not null + ? "[D.2b] vitals dat-font 0x40000000 loaded for numeric overlay." + : "[D.2b] vitals dat-font 0x40000000 unavailable — falling back to debug font."); + + // Phase D.2b — the vitals window is data-driven from the dat LayoutDesc + // (0x2100006C) via the LayoutImporter. The former hand-authored vitals.xml + // markup path was retired after the importer proved pixel-identical at the + // 2026-06-15 A/B gate. MarkupDocument stays for plugin/custom panels. + AcDream.App.UI.Layout.ImportedLayout? imported; + lock (_datLock) + imported = AcDream.App.UI.Layout.LayoutImporter.Import( + _dats!, 0x2100006Cu, ResolveChrome, vitalsDatFont); + if (imported is not null) { - foreach (var child in panel.Children) - if (child is AcDream.App.UI.UiMeter meter) - meter.DatFont = vitalsDatFont; - Console.WriteLine("[D.2b] vitals dat-font 0x40000000 loaded for numeric overlay."); + AcDream.App.UI.Layout.VitalsController.Bind(imported, + healthPct: () => _vitalsVm!.HealthPercent, + staminaPct: () => _vitalsVm!.StaminaPercent ?? 0f, + manaPct: () => _vitalsVm!.ManaPercent ?? 0f, + healthText: () => (_vitalsVm!.HealthCurrent, _vitalsVm.HealthMax) is (uint c, uint m) ? $"{c}/{m}" : "", + staminaText: () => (_vitalsVm!.StaminaCurrent, _vitalsVm.StaminaMax) is (uint c, uint m) ? $"{c}/{m}" : "", + manaText: () => (_vitalsVm!.ManaCurrent, _vitalsVm.ManaMax) is (uint c, uint m) ? $"{c}/{m}" : ""); + // Top-level window: user-positioned (Anchors.None so the per-frame anchor + // pass doesn't reset it) + movable, like the retired hand-authored panel. + // Resize is left off — the dat stacked-vitals layout (0x2100006C) is + // fixed-size (chrome edges near-pinned); faithful grip/dragbar-driven + // resize is the Plan-2 window manager. + var vitalsRoot = imported.Root; + vitalsRoot.Left = 10; vitalsRoot.Top = 30; + vitalsRoot.ClickThrough = false; + vitalsRoot.Anchors = AcDream.App.UI.AnchorEdges.None; + vitalsRoot.Draggable = true; + _uiHost.Root.AddChild(vitalsRoot); + Console.WriteLine("[D.2b] retail UI active — vitals window from LayoutDesc importer (0x2100006C)."); } else { - Console.WriteLine("[D.2b] vitals dat-font 0x40000000 unavailable — falling back to debug font."); - } - - _uiHost.Root.AddChild(panel); - Console.WriteLine("[D.2b] retail UI active — vitals panel from vitals.xml markup."); - - // Phase D.2b — LayoutDesc importer A/B harness. When ACDREAM_RETAIL_UI_IMPORTER=1, - // build the SAME vitals window (0x2100006C) data-driven from the dat and place it beside - // the hand-authored one so the two can be compared pixel-for-pixel before the importer - // becomes the default. The hand-authored path above is untouched. - if (_options.RetailUiImporter) - { - AcDream.App.UI.Layout.ImportedLayout? imported; - lock (_datLock) - imported = AcDream.App.UI.Layout.LayoutImporter.Import( - _dats!, 0x2100006Cu, ResolveChrome, vitalsDatFont); - if (imported is not null) - { - AcDream.App.UI.Layout.VitalsController.Bind(imported, - healthPct: () => _vitalsVm!.HealthPercent, - staminaPct: () => _vitalsVm!.StaminaPercent ?? 0f, - manaPct: () => _vitalsVm!.ManaPercent ?? 0f, - healthText: () => (_vitalsVm!.HealthCurrent, _vitalsVm.HealthMax) is (uint c, uint m) ? $"{c}/{m}" : "", - staminaText: () => (_vitalsVm!.StaminaCurrent, _vitalsVm.StaminaMax) is (uint c, uint m) ? $"{c}/{m}" : "", - manaText: () => (_vitalsVm!.ManaCurrent, _vitalsVm.ManaMax) is (uint c, uint m) ? $"{c}/{m}" : ""); - // Offset beside the hand-authored window (at x=10) for an A/B visual comparison. - imported.Root.Left = 200; - imported.Root.Top = 30; - _uiHost.Root.AddChild(imported.Root); - Console.WriteLine("[D.2b] importer vitals window active (A/B vs hand-authored)."); - } - else - { - Console.WriteLine("[D.2b] importer vitals: LayoutDesc 0x2100006C not found."); - } + Console.WriteLine("[D.2b] vitals: LayoutDesc 0x2100006C not found — vitals unavailable."); } // Retail chat window — a draggable/resizable nine-slice frame hosting a diff --git a/src/AcDream.App/RuntimeOptions.cs b/src/AcDream.App/RuntimeOptions.cs index bff1f885..9be7601d 100644 --- a/src/AcDream.App/RuntimeOptions.cs +++ b/src/AcDream.App/RuntimeOptions.cs @@ -41,7 +41,6 @@ public sealed record RuntimeOptions( bool DumpLiveSpawns, int? LegacyStreamRadius, bool RetailUi, - bool RetailUiImporter, string? AcDir) { /// @@ -86,7 +85,6 @@ public sealed record RuntimeOptions( // top of the quality preset's radii. Null when unset or invalid. LegacyStreamRadius: TryParseNonNegativeInt(env("ACDREAM_STREAM_RADIUS")), RetailUi: IsExactlyOne(env("ACDREAM_RETAIL_UI")), - RetailUiImporter: IsExactlyOne(env("ACDREAM_RETAIL_UI_IMPORTER")), AcDir: NullIfEmpty(env("ACDREAM_AC_DIR"))); } diff --git a/src/AcDream.App/UI/assets/vitals.xml b/src/AcDream.App/UI/assets/vitals.xml deleted file mode 100644 index eb8dfcbd..00000000 --- a/src/AcDream.App/UI/assets/vitals.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - diff --git a/tests/AcDream.App.Tests/RuntimeOptionsRetailUiTests.cs b/tests/AcDream.App.Tests/RuntimeOptionsRetailUiTests.cs index 9c6b88a2..b18590ae 100644 --- a/tests/AcDream.App.Tests/RuntimeOptionsRetailUiTests.cs +++ b/tests/AcDream.App.Tests/RuntimeOptionsRetailUiTests.cs @@ -25,29 +25,4 @@ public class RuntimeOptionsRetailUiTests Assert.False(opts.RetailUi); Assert.Null(opts.AcDir); } - - [Fact] - public void Parse_ReadsRetailUiImporter_WhenSetToOne() - { - var env = new Dictionary - { - ["ACDREAM_RETAIL_UI_IMPORTER"] = "1", - }; - var opts = RuntimeOptions.Parse("dats", k => env.GetValueOrDefault(k)); - Assert.True(opts.RetailUiImporter); - } - - [Fact] - public void Parse_DefaultsRetailUiImporterOff_WhenUnsetOrOtherValue() - { - // Unset → false. - Assert.False(RuntimeOptions.Parse("dats", _ => null).RetailUiImporter); - - // Non-"1" values → false (mirrors RetailUi / other IsExactlyOne flags). - var envOther = new Dictionary - { - ["ACDREAM_RETAIL_UI_IMPORTER"] = "true", - }; - Assert.False(RuntimeOptions.Parse("dats", k => envOther.GetValueOrDefault(k)).RetailUiImporter); - } }