feat(D.2b): flip vitals to the LayoutDesc importer; retire hand-authored vitals.xml

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) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-15 16:30:24 +02:00
parent 5ac9d8c19c
commit bf77a23ad3
4 changed files with 37 additions and 89 deletions

View file

@ -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

View file

@ -41,7 +41,6 @@ public sealed record RuntimeOptions(
bool DumpLiveSpawns,
int? LegacyStreamRadius,
bool RetailUi,
bool RetailUiImporter,
string? AcDir)
{
/// <summary>
@ -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")));
}

View file

@ -1,13 +0,0 @@
<!-- Retail stacked vitals window, geometry + sprite ids dat-verified from
LayoutDesc 0x2100006C (160x58, 5px chrome border, three flush 150x16 bars
at y=5/21/37). Each bar: back 3-slice (empty track) + front 3-slice (fill,
clipped to the fraction). Sprite ids are the STACKED-window set
(0x0600747E-0x0600748F), NOT the floaty-row set. -->
<panel id="acdream.vitals" x="10" y="30" w="160" h="58" resize="x">
<meter id="health" x="5" y="5" w="150" h="16" fill="{HealthPercent}" cur="{HealthCurrent}" max="{HealthMax}" color="#FFC70D0D" anchor="left,top,right"
backleft="0x0600747E" backtile="0x0600747F" backright="0x06007480" frontleft="0x06007481" fronttile="0x06007482" frontright="0x06007483"/>
<meter id="stamina" x="5" y="21" w="150" h="16" fill="{StaminaPercent}" cur="{StaminaCurrent}" max="{StaminaMax}" color="#FFD49E1F" anchor="left,top,right"
backleft="0x06007484" backtile="0x06007485" backright="0x06007486" frontleft="0x06007487" fronttile="0x06007488" frontright="0x06007489"/>
<meter id="mana" x="5" y="37" w="150" h="16" fill="{ManaPercent}" cur="{ManaCurrent}" max="{ManaMax}" color="#FF1F33D9" anchor="left,top,right"
backleft="0x0600748A" backtile="0x0600748B" backright="0x0600748C" frontleft="0x0600748D" fronttile="0x0600748E" frontright="0x0600748F"/>
</panel>

View file

@ -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<string, string?>
{
["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<string, string?>
{
["ACDREAM_RETAIL_UI_IMPORTER"] = "true",
};
Assert.False(RuntimeOptions.Parse("dats", k => envOther.GetValueOrDefault(k)).RetailUiImporter);
}
}