feat(chat): retail text style — two-plane glyph outlines, authored SpewBox/chat styles
Campaign CH round 4, user-gate items 1+2. Root cause: retail ships a second (background) glyph atlas per font, dilated 2px on every side, plus two border-pixel scalars (Font.NumHorizontalBorderPixels/ NumVerticalBorderPixels) that acdream's font reader never read — so even the pre-existing outline parameter drew almost nothing once enabled. Landed together (either half alone is a no-op or a regression): - UiDatFont carries BorderX/BorderY from the DAT font resource. - UiRenderContext.DrawStringDat inflates the background blit's source and destination rect by that margin and restructures into retail's exact two-pass whole-string outline-then-fill model (UIElement_Text::DrawSelf), plus the 8-neighbour +-1px fallback for fonts with no background atlas. Corrects the stale "property 0xd" comment to the real ids, 0x21 (Outline) / 0x22 (OutlineColor). - LayoutDesc property 0x21/0x22 import (ElementInfo.Outline/ OutlineColor, LayoutImporter.ReadState, ElementReader.Merge/ ApplyCanonicalLegacyProjection, DatWidgetFactory.BuildText) so every authored-outline element across the DAT set is correct at once. - SpewBox: RetailFontId corrected from a round-3 heuristic (0x40000025) to the actually-authored 0x40000001 (18px bold serif), Outline=true set on the controller's UiText. Fill colour stays the user-gate-round-1-pinned yellow — font atlases are alpha-only (PFID_A8), so there is no baked shading that could explain the screenshot's gold as anything other than the outline itself. - Chat transcript: default fill now seeds from its authored ARGB(255,204,204,204) instead of an unrelated color-table slot (ChatTranscriptRenderer.BuildLines takes the transcript's own DefaultColor as a parameter); the 34-entry LogTextType table is untouched, and every existing CH1 conformance test stays green unmodified. Regenerated the committed chat_2100006f.json fixture from the real installed DAT, confirming end to end (not by missing-field default) that the transcript carries no outline. Tests: font-reader border fields + inflation math pinned against the real DAT font, two-pass draw ordering/tint/inflation via a new TextRenderer.DebugSpriteSegmentVerts test seam, property 0x21/0x22 import at both the ElementReader.Merge and StateDesc-property layers, SpewBox font/outline, and the chat default-shade seed with the color table proven untouched. Full Release suite: 12,610 passed / 4 skipped / 0 failed (AcDream.slnx, complete solution). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
5b54387b8e
commit
bcc34ee301
22 changed files with 1995 additions and 105 deletions
|
|
@ -12,16 +12,22 @@ namespace AcDream.App.UI;
|
|||
/// <summary>
|
||||
/// 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
|
||||
/// background outline/shadow), the per-glyph descriptor table, the border-pixel
|
||||
/// inflation margin (<see cref="BorderX"/>/<see cref="BorderY"/>), and the line
|
||||
/// metrics, so <see cref="UiRenderContext.DrawStringDat"/> can blit each glyph
|
||||
/// as two textured quads exactly the way the retail client does.
|
||||
///
|
||||
/// <para>
|
||||
/// Retail render model — <c>SurfaceWindow::DrawCharacter</c>
|
||||
/// (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
|
||||
/// Retail render model — <c>UIElement_Text::DrawSelf</c> (acclient 0x00467aa0)
|
||||
/// runs the WHOLE glyph run TWICE when the element's outline flag (LayoutDesc
|
||||
/// property 0x21, <c>m_bitField & 0x10</c>) is set: pass 0 draws every glyph's
|
||||
/// outline (the BACKGROUND atlas sub-rect, inflated by <see cref="BorderX"/>/
|
||||
/// <see cref="BorderY"/> 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. <c>SurfaceWindow::DrawCharacter</c>
|
||||
/// (acclient 0x00442bd0, Font::GetCharDesc + the two SurfaceWindow blits) is the
|
||||
/// per-glyph blit each pass calls. The pen advances by
|
||||
/// <c>HorizontalOffsetBefore + Width + HorizontalOffsetAfter</c> (the function's
|
||||
/// return value, accumulated by the string loop at 0x00467ed4
|
||||
/// <c>edi_3 += var_98</c>), and each glyph is drawn starting at
|
||||
|
|
@ -34,8 +40,8 @@ namespace AcDream.App.UI;
|
|||
/// (255,255,255, alpha). The UI sprite shader path (ui_text.frag,
|
||||
/// <c>uUseTexture==2</c>) MULTIPLIES the sampled texel by the per-vertex tint
|
||||
/// (<c>texture(uTex,vUv) * vColor</c>), 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.
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class UiDatFont
|
||||
|
|
@ -60,19 +66,38 @@ public sealed class UiDatFont
|
|||
/// <summary>Distance from a line's top to its baseline (retail BaselineOffset).</summary>
|
||||
public float BaselineOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>Font::m_NumHorizontalBorderPixels</c> / <c>m_NumVerticalBorderPixels</c>
|
||||
/// (<c>Font::Serialize @0x00443650</c>; verified struct offsets <c>Font+0x40</c> /
|
||||
/// <c>Font+0x44</c>). 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 <c>(BorderX, BorderY)</c> inside its inflated window,
|
||||
/// and the BG glyph's alpha bbox begins at <c>(BorderX-2, BorderY-2)</c>. Zero for
|
||||
/// fonts with no background atlas (e.g. the CJK/unicode family, <see cref="HasBackground"/>
|
||||
/// is false). <see cref="UiRenderContext.DrawStringDat"/> inflates the background blit's
|
||||
/// source AND destination rect by exactly this much on every side
|
||||
/// (<c>SurfaceWindow::DrawCharacter @0x00442d3a</c> + <c>CreateCharRectPair @0x00441480</c>)
|
||||
/// so the dilation is actually captured instead of cropped away.
|
||||
/// </summary>
|
||||
public int BorderX { get; }
|
||||
public int BorderY { get; }
|
||||
|
||||
private readonly Dictionary<char, FontCharDesc> _glyphs;
|
||||
|
||||
internal UiDatFont(
|
||||
uint fgTex, int fgW, int fgH,
|
||||
uint bgTex, int bgW, int bgH,
|
||||
float lineHeight, float baselineOffset,
|
||||
Dictionary<char, FontCharDesc> glyphs)
|
||||
Dictionary<char, FontCharDesc> 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;
|
||||
}
|
||||
|
||||
/// <summary>True if this font carries a separate outline/shadow atlas
|
||||
|
|
@ -123,7 +148,9 @@ public sealed class UiDatFont
|
|||
bgTex, bgW, bgH,
|
||||
lineHeight: font.MaxCharHeight,
|
||||
baselineOffset: font.BaselineOffset,
|
||||
glyphs);
|
||||
glyphs,
|
||||
borderX: (int)font.NumHorizontalBorderPixels,
|
||||
borderY: (int)font.NumVerticalBorderPixels);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue