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>
84 lines
2.9 KiB
C#
84 lines
2.9 KiB
C#
using System.Collections.Generic;
|
|
using AcDream.App.UI;
|
|
using DatReaderWriter.Types;
|
|
|
|
namespace AcDream.App.Tests.UI;
|
|
|
|
/// <summary>
|
|
/// Pure pen-advance / MeasureWidth math for the retail dat font (no GL, no dat).
|
|
/// The advance per glyph is the retail
|
|
/// <c>HorizontalOffsetBefore + Width + HorizontalOffsetAfter</c>
|
|
/// (SurfaceWindow::DrawCharacter, acclient 0x00442c3a), accumulated across the
|
|
/// string the way the retail string loop does (0x00467ed4 edi_3 += var_98).
|
|
/// </summary>
|
|
public class UiDatFontTests
|
|
{
|
|
private static FontCharDesc Glyph(
|
|
ushort unicode, byte width,
|
|
sbyte before = 0, sbyte after = 0,
|
|
ushort offsetX = 0, ushort offsetY = 0, byte height = 16, sbyte vBefore = 0)
|
|
=> new()
|
|
{
|
|
Unicode = unicode,
|
|
Width = width,
|
|
Height = height,
|
|
OffsetX = offsetX,
|
|
OffsetY = offsetY,
|
|
HorizontalOffsetBefore = before,
|
|
HorizontalOffsetAfter = after,
|
|
VerticalOffsetBefore = vBefore,
|
|
};
|
|
|
|
[Fact]
|
|
public void GlyphAdvance_SumsBeforeWidthAfter()
|
|
{
|
|
var g = Glyph('A', width: 8, before: 1, after: 2);
|
|
Assert.Equal(11f, UiDatFont.GlyphAdvance(g));
|
|
}
|
|
|
|
[Fact]
|
|
public void GlyphAdvance_HandlesNegativeBearings()
|
|
{
|
|
// Kerned glyph: a negative left-bearing pulls it leftward; the advance
|
|
// still nets out to before + width + after.
|
|
var g = Glyph('j', width: 4, before: -1, after: 0);
|
|
Assert.Equal(3f, UiDatFont.GlyphAdvance(g));
|
|
}
|
|
|
|
[Fact]
|
|
public void MeasureWidth_SumsEachGlyphAdvance()
|
|
{
|
|
var table = new Dictionary<char, FontCharDesc>
|
|
{
|
|
['2'] = Glyph('2', width: 7, before: 1, after: 1), // advance 9
|
|
['9'] = Glyph('9', width: 7, before: 1, after: 1), // advance 9
|
|
['1'] = Glyph('1', width: 3, before: 2, after: 1), // advance 6
|
|
['/'] = Glyph('/', width: 4, before: 0, after: 1), // advance 5
|
|
};
|
|
FontCharDesc? Lookup(char c) => table.TryGetValue(c, out var g) ? g : null;
|
|
|
|
// "291/291" = 9 + 9 + 6 + 5 + 9 + 9 + 6 = 53
|
|
Assert.Equal(53f, UiDatFont.MeasureWidth("291/291", Lookup));
|
|
}
|
|
|
|
[Fact]
|
|
public void MeasureWidth_SkipsCharactersNotInFont()
|
|
{
|
|
var table = new Dictionary<char, FontCharDesc>
|
|
{
|
|
['5'] = Glyph('5', width: 6, before: 1, after: 1), // advance 8
|
|
};
|
|
FontCharDesc? Lookup(char c) => table.TryGetValue(c, out var g) ? g : null;
|
|
|
|
// 'X' has no glyph → contributes nothing; only the two '5's count.
|
|
Assert.Equal(16f, UiDatFont.MeasureWidth("5X5", Lookup));
|
|
}
|
|
|
|
[Fact]
|
|
public void MeasureWidth_EmptyOrNullIsZero()
|
|
{
|
|
FontCharDesc? Lookup(char c) => null;
|
|
Assert.Equal(0f, UiDatFont.MeasureWidth("", Lookup));
|
|
Assert.Equal(0f, UiDatFont.MeasureWidth(null, Lookup));
|
|
}
|
|
}
|