using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Types; namespace AcDream.App.UI; /// /// A retail dat-font (DB_TYPE_FONT, id range 0x40000000-0x40000FFF) ready for /// 2D drawing. Holds the two GL atlas textures (foreground glyph pixels + /// background outline/shadow), the per-glyph descriptor table, and the line /// metrics, so can blit each glyph /// as two textured quads exactly the way the retail client does. /// /// /// Retail render model — SurfaceWindow::DrawCharacter /// (acclient 0x00442bd0, Font::GetCharDesc + the two SurfaceWindow blits): for /// each glyph it copies the BACKGROUND atlas sub-rect first, tinted with the /// outline color (black), then the FOREGROUND atlas sub-rect, tinted with the /// requested text color. The pen advances by /// HorizontalOffsetBefore + Width + HorizontalOffsetAfter (the function's /// return value, accumulated by the string loop at 0x00467ed4 /// edi_3 += var_98), and each glyph is drawn starting at /// penX + HorizontalOffsetBefore. /// /// /// /// Atlas format: the foreground atlas (0x06005EE5 for Font 0x40000000) is /// PFID_A8 — alpha-only. Our SurfaceDecoder expands A8 to RGBA as /// (255,255,255, alpha). The UI sprite shader path (ui_text.frag, /// uUseTexture==2) MULTIPLIES the sampled texel by the per-vertex tint /// (texture(uTex,vUv) * vColor), so tinting a white+alpha glyph by a /// color gives that color with the glyph's alpha — black for the outline pass, /// text color for the fill pass. No shader change was needed. /// /// public sealed class UiDatFont { /// Retail UI font id (Latin-1, 16x16 max, with outline atlas). public const uint DefaultFontId = 0x40000000u; /// Foreground (glyph pixels) GL texture handle + atlas pixel size. public uint ForegroundTexture { get; } public int ForegroundWidth { get; } public int ForegroundHeight { get; } /// Background (outline/shadow) GL texture handle + atlas pixel size. /// 0 when the font has no background atlas (then the outline pass is skipped). public uint BackgroundTexture { get; } public int BackgroundWidth { get; } public int BackgroundHeight { get; } /// Vertical advance between lines (retail MaxCharHeight). public float LineHeight { get; } /// Distance from a line's top to its baseline (retail BaselineOffset). public float BaselineOffset { get; } private readonly Dictionary _glyphs; private UiDatFont( uint fgTex, int fgW, int fgH, uint bgTex, int bgW, int bgH, float lineHeight, float baselineOffset, Dictionary glyphs) { ForegroundTexture = fgTex; ForegroundWidth = fgW; ForegroundHeight = fgH; BackgroundTexture = bgTex; BackgroundWidth = bgW; BackgroundHeight = bgH; LineHeight = lineHeight; BaselineOffset = baselineOffset; _glyphs = glyphs; } /// True if this font carries a separate outline/shadow atlas /// (retail's m_pBackgroundSurface). When false the outline pass is /// skipped and only the foreground (fill) glyphs are drawn. public bool HasBackground => BackgroundTexture != 0; /// Look up a glyph descriptor for a character. Returns false for /// characters not present in the font's table (callers skip them). public bool TryGetGlyph(char c, out FontCharDesc glyph) => _glyphs.TryGetValue(c, out glyph!); /// /// Load Font from the dat collection and upload /// both atlases through the texture cache (the same direct-RenderSurface /// path the D.2b chrome sprites use). Returns null if the Font DBObj is /// missing — callers fall back to the debug bitmap font. /// public static UiDatFont? Load(DatCollection dats, TextureCache cache, uint fontId = DefaultFontId) { ArgumentNullException.ThrowIfNull(dats); ArgumentNullException.ThrowIfNull(cache); if (!dats.TryGet(fontId, out var font) || font is null) return null; // Foreground atlas is required; without it there are no glyph pixels. if (font.ForegroundSurfaceDataId == 0) return null; uint fgTex = cache.GetOrUploadRenderSurface(font.ForegroundSurfaceDataId, out int fgW, out int fgH); uint bgTex = 0; int bgW = 0, bgH = 0; if (font.BackgroundSurfaceDataId != 0) bgTex = cache.GetOrUploadRenderSurface(font.BackgroundSurfaceDataId, out bgW, out bgH); // Build the char->descriptor lookup. FontCharDesc.Unicode is the code // point; for Latin-1 fonts this is a direct char cast. Last write wins // on the rare duplicate (retail's Font::GetCharDesc does a linear scan // and returns the first match, but the dat tables have no duplicates). var glyphs = new Dictionary(font.CharDescs.Count); foreach (var cd in font.CharDescs) glyphs[(char)cd.Unicode] = cd; return new UiDatFont( fgTex, fgW, fgH, bgTex, bgW, bgH, lineHeight: font.MaxCharHeight, baselineOffset: font.BaselineOffset, glyphs); } /// /// Total pen advance (in pixels) for , summing each /// glyph's retail advance. Characters not in the font contribute nothing. /// public float MeasureWidth(string text) => MeasureWidth(text, c => _glyphs.TryGetValue(c, out var g) ? g : null); /// /// Pure pen-advance summation seam: total width of /// given a that maps each char to its descriptor /// (null = not in the font → contributes nothing). Lets the advance math be /// unit-tested with synthetic glyphs, with no GL or dat dependency. /// public static float MeasureWidth(string? text, Func lookup) { ArgumentNullException.ThrowIfNull(lookup); if (string.IsNullOrEmpty(text)) return 0f; float w = 0f; for (int i = 0; i < text.Length; i++) if (lookup(text[i]) is { } g) w += GlyphAdvance(g); return w; } /// /// The retail per-glyph horizontal advance: /// HorizontalOffsetBefore + Width + HorizontalOffsetAfter. This is the /// value SurfaceWindow::DrawCharacter returns for proportional text /// (flag bit 0x10 set, acclient 0x00442c3a) and the string loop accumulates /// into the pen. Pulled out as a pure static so the math is unit-testable /// without GL or the dat. /// public static float GlyphAdvance(FontCharDesc g) => g.HorizontalOffsetBefore + g.Width + g.HorizontalOffsetAfter; }