feat(D.2b): retail dat-font (Font 0x40000000) for vitals numbers

The vitals cur/max overlay rendered with the consola TTF debug font,
which is wrong for the retail look. Port the retail dat-font render
path so the numbers use Font 0x40000000 (Latin-1, 16px, with outline
atlas) — the same font retail draws on the vitals window.

UiDatFont (new): loads the Font DBObj from the DatCollection and
uploads its two RenderSurface atlases (foreground glyph pixels
0x06005EE5 + background outline 0x06005EE6) through
TextureCache.GetOrUploadRenderSurface — the same direct-RenderSurface
path the D.2b chrome sprites use. Builds a char->FontCharDesc lookup
and exposes MeasureWidth + LineHeight. The per-glyph advance
(HorizontalOffsetBefore + Width + HorizontalOffsetAfter) is a pure
static so the pen math is unit-testable without GL or the dat.

UiRenderContext.DrawStringDat (new): two-pass per-glyph blit mirroring
SurfaceWindow::DrawCharacter (acclient 0x00442bd0) — the BACKGROUND
atlas sub-rect tinted black (outline) first, then the FOREGROUND
sub-rect tinted the text color (fill), with the pen accumulating the
retail advance the way the string loop does at 0x00467ed4. Respects
the UI transform stack. Skips the outline pass for fonts with no
background atlas.

No shader change was needed: the foreground atlas decodes A8 ->
(255,255,255,a), and ui_text.frag's RGBA-sprite path already
MULTIPLIES the texel by the per-vertex tint (texture(uTex,vUv)*vColor),
so tinting white+alpha by a color gives color+alpha (black outline,
text-color fill).

UiMeter: new DatFont property; the label renders via DrawStringDat
(centered with DatFont.MeasureWidth) when set, falling back to the
debug BitmapFont when null.

GameWindow: loads one UiDatFont for the vitals panel (under _datLock)
and assigns it to each UiMeter child; logs + falls back to the debug
font if the Font fails to load (never crashes).

Tests: 6 pure-logic UiDatFontTests for GlyphAdvance + MeasureWidth
(synthetic glyphs, negative bearings, missing chars, empty/null). Full
App UI suite green (84 passed).

DatReaderWriter member names verified via reflection on the 2.1.7
package: Font.{MaxCharHeight,BaselineOffset,ForegroundSurfaceDataId,
BackgroundSurfaceDataId,CharDescs} and FontCharDesc.{Unicode,OffsetX,
OffsetY,Width,Height,HorizontalOffsetBefore,HorizontalOffsetAfter,
VerticalOffsetBefore}.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-14 23:02:35 +02:00
parent ff29787f12
commit 36bd3522f4
5 changed files with 361 additions and 5 deletions

View file

@ -24,6 +24,12 @@ public sealed class UiMeter : UiElement
public Vector4 BgColor { get; set; } = new(0f, 0f, 0f, 0.5f);
public Vector4 LabelColor { get; set; } = new(1f, 1f, 1f, 1f);
/// <summary>Retail dat font (Font 0x40000000) for the "cur/max" overlay. When
/// set, the label renders through the dat-font two-pass blit (outline + fill);
/// when null, the debug <see cref="UiRenderContext.DefaultFont"/> bitmap font
/// is used instead. Set by the host when the retail UI is active.</summary>
public UiDatFont? DatFont { get; set; }
/// <summary>Resolver from a RenderSurface DataId to (GL handle, w, h). When set
/// with the 9-slice ids below, the bar draws the retail sprites instead of solid color.</summary>
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
@ -87,12 +93,24 @@ public sealed class UiMeter : UiElement
}
string? label = Label();
if (!string.IsNullOrEmpty(label) && ctx.DefaultFont is { } font)
if (!string.IsNullOrEmpty(label))
{
float tw = font.MeasureWidth(label);
float tx = (Width - tw) * 0.5f;
float ty = (Height - font.LineHeight) * 0.5f;
ctx.DrawString(label, tx, ty, LabelColor);
if (DatFont is { } datFont)
{
// Retail path: centered cur/max via the dat font's two-pass blit.
float tw = datFont.MeasureWidth(label);
float tx = (Width - tw) * 0.5f;
float ty = (Height - datFont.LineHeight) * 0.5f;
ctx.DrawStringDat(datFont, label, tx, ty, LabelColor);
}
else if (ctx.DefaultFont is { } font)
{
// Fallback: debug bitmap font (no dat font available).
float tw = font.MeasureWidth(label);
float tx = (Width - tw) * 0.5f;
float ty = (Height - font.LineHeight) * 0.5f;
ctx.DrawString(label, tx, ty, LabelColor);
}
}
}