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;
}