acdream/src/AcDream.App/UI
Erik 36bd3522f4 feat(D.2b): retail dat-font (Font 0x40000000) for vitals numbers
The vitals cur/max overlay rendered with the consola TTF debug font,
which is wrong for the retail look. Port the retail dat-font render
path so the numbers use Font 0x40000000 (Latin-1, 16px, with outline
atlas) — the same font retail draws on the vitals window.

UiDatFont (new): loads the Font DBObj from the DatCollection and
uploads its two RenderSurface atlases (foreground glyph pixels
0x06005EE5 + background outline 0x06005EE6) through
TextureCache.GetOrUploadRenderSurface — the same direct-RenderSurface
path the D.2b chrome sprites use. Builds a char->FontCharDesc lookup
and exposes MeasureWidth + LineHeight. The per-glyph advance
(HorizontalOffsetBefore + Width + HorizontalOffsetAfter) is a pure
static so the pen math is unit-testable without GL or the dat.

UiRenderContext.DrawStringDat (new): two-pass per-glyph blit mirroring
SurfaceWindow::DrawCharacter (acclient 0x00442bd0) — the BACKGROUND
atlas sub-rect tinted black (outline) first, then the FOREGROUND
sub-rect tinted the text color (fill), with the pen accumulating the
retail advance the way the string loop does at 0x00467ed4. Respects
the UI transform stack. Skips the outline pass for fonts with no
background atlas.

No shader change was needed: the foreground atlas decodes A8 ->
(255,255,255,a), and ui_text.frag's RGBA-sprite path already
MULTIPLIES the texel by the per-vertex tint (texture(uTex,vUv)*vColor),
so tinting white+alpha by a color gives color+alpha (black outline,
text-color fill).

UiMeter: new DatFont property; the label renders via DrawStringDat
(centered with DatFont.MeasureWidth) when set, falling back to the
debug BitmapFont when null.

GameWindow: loads one UiDatFont for the vitals panel (under _datLock)
and assigns it to each UiMeter child; logs + falls back to the debug
font if the Font fails to load (never crashes).

Tests: 6 pure-logic UiDatFontTests for GlyphAdvance + MeasureWidth
(synthetic glyphs, negative bearings, missing chars, empty/null). Full
App UI suite green (84 passed).

DatReaderWriter member names verified via reflection on the 2.1.7
package: Font.{MaxCharHeight,BaselineOffset,ForegroundSurfaceDataId,
BackgroundSurfaceDataId,CharDescs} and FontCharDesc.{Unicode,OffsetX,
OffsetY,Width,Height,HorizontalOffsetBefore,HorizontalOffsetAfter,
VerticalOffsetBefore}.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 23:02:35 +02:00
..
assets fix(D.2b): vitals from the real stacked-window LayoutDesc (0x2100006C) 2026-06-14 22:50:17 +02:00
ControlsIni.cs feat(D.2b): controls.ini stylesheet loader + apply title color 2026-06-14 17:31:55 +02:00
MarkupDocument.cs feat(D.2b): retail 3-slice vital bars + headless mockup verifier 2026-06-14 21:40:11 +02:00
README.md docs+feat(ui): retail UI deep-dive research + C# port scaffold 2026-04-17 19:13:02 +02:00
RetailChromeSprites.cs feat(D.2b): Step-0 chrome sprites confirmed + direct-RenderSurface upload path 2026-06-14 16:32:27 +02:00
TargetIndicatorPanel.cs feat(retail): Commit B — retail-faithful AP cadence + screen-rect picker 2026-05-16 13:56:08 +02:00
UiChatView.cs feat(D.2b): scrollable retail chat window (read-only foundation) 2026-06-14 22:12:12 +02:00
UiDatFont.cs feat(D.2b): retail dat-font (Font 0x40000000) for vitals numbers 2026-06-14 23:02:35 +02:00
UiElement.cs feat(D.2b): anchor layout — vital bars stretch with window; drop Vitals heading 2026-06-14 18:58:58 +02:00
UiEvent.cs docs+feat(ui): retail UI deep-dive research + C# port scaffold 2026-04-17 19:13:02 +02:00
UiHost.cs docs+feat(ui): retail UI deep-dive research + C# port scaffold 2026-04-17 19:13:02 +02:00
UiMeter.cs feat(D.2b): retail dat-font (Font 0x40000000) for vitals numbers 2026-06-14 23:02:35 +02:00
UiNineSlicePanel.cs fix(D.2b): windows not anchor-managed (regression: move/resize was reset each frame) 2026-06-14 19:06:58 +02:00
UiPanel.cs docs+feat(ui): retail UI deep-dive research + C# port scaffold 2026-04-17 19:13:02 +02:00
UiRenderContext.cs feat(D.2b): retail dat-font (Font 0x40000000) for vitals numbers 2026-06-14 23:02:35 +02:00
UiRoot.cs feat(D.2b): per-window resize-axis lock; vitals window is X-only (retail) 2026-06-14 18:51:56 +02:00

AcDream.App.UI — Retail-style UI toolkit

This is acdream's retained-mode UI toolkit. It mirrors the behavior of the retail AC client (hit-testing, modal, capture, drag-drop, tooltip delay, focus routing, event type codes) without trying to byte-match the retail binary — because the retail widgets live in keystone.dll, which we don't decompile.

Research

All design decisions in this directory are grounded in the master synthesis + six deep-dive docs under docs/research/retail-ui/:

Document Topic
00-master-synthesis.md Cross-slice synthesis + C# port plan
01-architecture-and-init.md Process entry, window, main loop
02-class-hierarchy.md CUIManager / CUIListener / CFont / CSurface
03-rendering.md Font atlas, 2D quad batch, cursor
04-input-events.md WndProc → Device → widget event routing
05-panels.md Chat, attributes, spells, paperdoll, inventory
06-hud-and-assets.md Vital orbs, radar, compass + dat asset catalog

Files

  • UiEvent.cs — 24-byte event struct + retail-faithful type constants (0x01 click, 0x15 drag-begin, 0x3E drop, 0x201 WM_LBUTTONDOWN, …)
  • UiElement.cs — base widget with OnDraw / OnEvent / OnHitTest / OnTick virtuals, children list, ZOrder, focus/capture flags
  • UiPanel.csUiPanel (rect + optional bg/border), UiLabel, UiButton
  • UiRenderContext.cs — per-frame draw context with translate stack
  • UiRoot.cs — top-of-tree + "Device" responsibilities (mouse/keyboard state, focus, modal, capture, drag-drop, tooltip timer). Mirrors the retail DAT_00837ff4 Device object's vtable.
  • UiHost.cs — one-shot wrapper: owns the UiRoot, a TextRenderer, and a default BitmapFont. Provides WireMouse / WireKeyboard helpers for Silk.NET plumbing.

Integration pattern

// GameWindow.OnLoad
_uiHost = new UiHost(_gl!, shadersDir, _debugFont);
_uiHost.Root.WorldMouseFallThrough += (btn, x, y, flags) => HandleWorldClick(btn, x, y, flags);
_uiHost.Root.WorldKeyFallThrough   += (vk, lp) => HandleHotkey(vk);
foreach (var mouse in _input.Mice) _uiHost.WireMouse(mouse);
foreach (var kb in _input.Keyboards) _uiHost.WireKeyboard(kb);

// Add panels
var chat = new Panels.ChatWindow { Left = 10, Top = 400, Width = 500, Height = 250 };
_uiHost.Root.AddChild(chat);

// GameWindow.OnRender — after the 3D scene
_uiHost.Tick(deltaSeconds);
_uiHost.Draw(new Vector2(_window!.Size.X, _window.Size.Y));

What's scaffolded vs what still needs building

Shipped in the scaffold (this session)

  • UI tree + event routing + focus + modal + capture + drag-drop
  • Hit-testing (children-first, Z-order tie-break)
  • Tooltip timer (~1000ms)
  • Hover enter/leave, click vs right-click, scroll, keyboard
  • World fall-through so existing camera/player controls still work
  • Simple text/rect drawing through the existing BitmapFont + TextRenderer pipeline

To build next

  1. AcFont + FontCache — load Font DBObjs from portal.dat range 0x40000000..0x40000FFF, bake 256×256 glyph atlas from the referenced RenderSurface (0x06xxxxxx). See slice 03 §4.
  2. Dat sprite loader — decode RenderSurface dats as GL textures; add DrawSprite(uint datId, Rectangle dest, uint rgba) to UiRenderContext.
  3. CursorManager — OS cursor + dat-sourced custom cursors via slice 03 §7.
  4. Scissor clipping — for panels with scrollable interiors (chat, inventory grid). GL_SCISSOR_TEST wrapped in UiRenderContext.PushScissor / PopScissor.
  5. First concrete panel — ChatWindow since we have all 6 wire messages parsed already. See slice 05 §1.
  6. Vital orbs HUD once the server sends GameMessagePrivateUpdateVital. See slice 06 A.1.

Retail magic numbers the scaffold preserves

Because hand-ported panel code will copy the retail switch-case structure, we keep the magic constants:

// Event types
UiEventType.Click          == 0x01   // chunk_00470000.c ~11140
UiEventType.Tooltip        == 0x07   // chunk_00460000.c ~6253 (~1000ms delay)
UiEventType.DragBegin      == 0x15   // chunk_004A0000.c ~2707
UiEventType.DragEnter      == 0x21   // chunk_004A0000.c ~2714
UiEventType.DragOver       == 0x1C   // chunk_004A0000.c ~2723
UiEventType.DropReleased   == 0x3E   // chunk_004A0000.c ~2754
UiEventType.MouseDown      == 0x201  // WM_LBUTTONDOWN
UiEventType.MouseUp        == 0x202  // WM_LBUTTONUP

// Event IDs
// Widget event IDs live in the 0x10000000+ range (retail convention).
// UiRoot auto-assigns EventIds starting at 0x10000001.