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