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>
|
||
|---|---|---|
| .. | ||
| assets | ||
| ControlsIni.cs | ||
| MarkupDocument.cs | ||
| README.md | ||
| RetailChromeSprites.cs | ||
| TargetIndicatorPanel.cs | ||
| UiChatView.cs | ||
| UiDatFont.cs | ||
| UiElement.cs | ||
| UiEvent.cs | ||
| UiHost.cs | ||
| UiMeter.cs | ||
| UiNineSlicePanel.cs | ||
| UiPanel.cs | ||
| UiRenderContext.cs | ||
| UiRoot.cs | ||
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 (0x01click,0x15drag-begin,0x3Edrop,0x201WM_LBUTTONDOWN, …)UiElement.cs— base widget withOnDraw/OnEvent/OnHitTest/OnTickvirtuals, children list, ZOrder, focus/capture flagsUiPanel.cs—UiPanel(rect + optional bg/border),UiLabel,UiButtonUiRenderContext.cs— per-frame draw context with translate stackUiRoot.cs— top-of-tree + "Device" responsibilities (mouse/keyboard state, focus, modal, capture, drag-drop, tooltip timer). Mirrors the retailDAT_00837ff4Device object's vtable.UiHost.cs— one-shot wrapper: owns theUiRoot, aTextRenderer, and a defaultBitmapFont. ProvidesWireMouse/WireKeyboardhelpers 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+TextRendererpipeline
To build next
AcFont+FontCache— loadFontDBObjs fromportal.datrange0x40000000..0x40000FFF, bake 256×256 glyph atlas from the referencedRenderSurface(0x06xxxxxx). See slice 03 §4.- Dat sprite loader — decode
RenderSurfacedats as GL textures; addDrawSprite(uint datId, Rectangle dest, uint rgba)toUiRenderContext. CursorManager— OS cursor + dat-sourced custom cursors via slice 03 §7.- Scissor clipping — for panels with scrollable interiors (chat,
inventory grid).
GL_SCISSOR_TESTwrapped inUiRenderContext.PushScissor/PopScissor. - First concrete panel —
ChatWindowsince we have all 6 wire messages parsed already. See slice 05 §1. - 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.