using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; using AcDream.Content; 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, the border-pixel /// inflation margin (/), and the line /// metrics, so can blit each glyph /// as two textured quads exactly the way the retail client does. /// /// /// Retail render model — UIElement_Text::DrawSelf (acclient 0x00467aa0) /// runs the WHOLE glyph run TWICE when the element's outline flag (LayoutDesc /// property 0x21, m_bitField & 0x10) is set: pass 0 draws every glyph's /// outline (the BACKGROUND atlas sub-rect, inflated by / /// on every side, tinted with the element's outline color — /// ctor default black, property 0x22), then pass 1 draws every glyph's fill (the /// FOREGROUND atlas sub-rect, tinted with the requested text color) on top. With /// the outline flag clear, only pass 1 runs. SurfaceWindow::DrawCharacter /// (acclient 0x00442bd0, Font::GetCharDesc + the two SurfaceWindow blits) is the /// per-glyph blit each pass calls. 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 — the outline color 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; } /// /// Retail Font::m_NumHorizontalBorderPixels / m_NumVerticalBorderPixels /// (Font::Serialize @0x00443650; verified struct offsets Font+0x40 / /// Font+0x44). The background (outline) atlas glyph is the foreground glyph /// dilated 2px on every side, sitting inside a margin this wide — measured: the FG /// glyph always sits at exactly (BorderX, BorderY) inside its inflated window, /// and the BG glyph's alpha bbox begins at (BorderX-2, BorderY-2). Zero for /// fonts with no background atlas (e.g. the CJK/unicode family, /// is false). inflates the background blit's /// source AND destination rect by exactly this much on every side /// (SurfaceWindow::DrawCharacter @0x00442d3a + CreateCharRectPair @0x00441480) /// so the dilation is actually captured instead of cropped away. /// public int BorderX { get; } public int BorderY { get; } private readonly Dictionary _glyphs; internal UiDatFont( uint fgTex, int fgW, int fgH, uint bgTex, int bgW, int bgH, float lineHeight, float baselineOffset, Dictionary glyphs, int borderX = 0, int borderY = 0) { ForegroundTexture = fgTex; ForegroundWidth = fgW; ForegroundHeight = fgH; BackgroundTexture = bgTex; BackgroundWidth = bgW; BackgroundHeight = bgH; LineHeight = lineHeight; BaselineOffset = baselineOffset; _glyphs = glyphs; BorderX = borderX; BorderY = borderY; } /// 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(IDatReaderWriter 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; // Point-sample the glyph atlases (nearest) so small UI text stays pixel-crisp; // bilinear softens the dat font noticeably (the chat menu/button text "blur"). uint fgTex = cache.GetOrUploadRenderSurface(font.ForegroundSurfaceDataId, out int fgW, out int fgH, nearest: true); uint bgTex = 0; int bgW = 0, bgH = 0; if (font.BackgroundSurfaceDataId != 0) bgTex = cache.GetOrUploadRenderSurface(font.BackgroundSurfaceDataId, out bgW, out bgH, nearest: true); // 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, borderX: (int)font.NumHorizontalBorderPixels, borderY: (int)font.NumVerticalBorderPixels); } /// /// Total pen advance (in pixels) for , summing each /// glyph's retail advance. Characters not in the font contribute nothing. /// public float MeasureWidth(string text) { if (string.IsNullOrEmpty(text)) return 0f; float width = 0f; for (int index = 0; index < text.Length; index++) { if (_glyphs.TryGetValue(text[index], out FontCharDesc? glyph)) width += GlyphAdvance(glyph); } return width; } /// /// 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; }