docs+feat(ui): retail UI deep-dive research + C# port scaffold

Deep investigation of the retail AC client's GUI subsystem, driven by 6
parallel Opus research agents, plus the first cut of a retail-faithful
retained-mode widget toolkit that scaffolds Phase D.

Research (docs/research/retail-ui/):
- 00-master-synthesis.md        — cross-slice synthesis + port plan
- 01-architecture-and-init.md   — WinMain, CreateMainWindow, frame loop,
                                  Keystone bring-up (7 globals mapped)
- 02-class-hierarchy.md         — key finding: UI lives in keystone.dll,
                                  not acclient.exe; CUIManager + CUIListener
                                  MI pattern, CFont + CSurface + CString
- 03-rendering.md               — 24-byte XYZRHW+UV verts, per-font
                                  256x256 atlas baked from RenderSurface,
                                  TEXTUREFACTOR coloring, DrawPrimitiveUP
- 04-input-events.md            — Win32 WndProc → Device (DAT_00837ff4)
                                  → widget OnEvent(+0x128); full event-type
                                  table (0x01 click, 0x07 tooltip ~1000ms,
                                  0x15 drag-begin, 0x21 enter, 0x3E drop)
- 05-panels.md                  — chat, attributes, skills, spells, paperdoll
                                  (25-slot layout), inventory, fellowship,
                                  allegiance — with wire-message bindings
- 06-hud-and-assets.md          — vital orbs (scissor fill), radar
                                  (0x06001388/0x06004CC1, 1.18× shrink),
                                  compass strip, dat asset catalog

Key insight: keystone.dll owns the actual widget toolkit — we cannot
port a class hierarchy from the decompile because it's not there.
Instead we implement our own retained-mode toolkit with retail-faithful
behavior (event codes, focus/modal/capture, drag-drop state machine)
and will consume the same portal.dat fonts + sprites so the visual
identity is preserved.

C# scaffold (src/AcDream.App/UI/):
- UiEvent          — 24-byte event struct + retail event-type constants
                     (0x01 click, 0x15 drag-begin, 0x201 WM_LBUTTONDOWN,
                     etc.) matching retail decompile switches
- UiElement        — base widget: children, ZOrder, focus/capture flags,
                     virtual OnDraw/OnEvent/OnHitTest/OnTick; children-
                     first hit test + back-to-front composite
- UiPanel          — panel, label, button primitives
- UiRenderContext  — 2D draw context with translate stack
- UiRoot           — top-of-tree + Device responsibilities (mouse/
                     keyboard state, focus, modal, capture, drag-drop,
                     tooltip timer); WorldMouseFallThrough/
                     WorldKeyFallThrough preserves existing camera
                     controls when no widget consumes
- UiHost           — packages UiRoot + TextRenderer + input wiring
                     helpers for one-line integration into GameWindow
- README.md        — orientation for future agents

Roadmap (docs/plans/2026-04-11-roadmap.md):
- D.1 marked shipped (debug overlay from 2026-04-17)
- D.2 expanded to include the retail UI framework landed here
- D.3-D.7 added: AcFont, dat sprites, core panels, HUD, CursorManager
- D.8 remains sound

All existing 470 tests pass. 0 warnings, 0 errors.
This commit is contained in:
Erik 2026-04-17 19:13:02 +02:00
parent ff325abd7b
commit 7230c1590f
15 changed files with 8041 additions and 5 deletions

View file

@ -0,0 +1,671 @@
# Retail AC Client GUI — Master Synthesis
**Date:** 2026-04-17
**Sources:** 6 parallel Opus research passes over the decompiled
`acclient.exe` (22,225 functions / 688K lines). Individual deep dives:
- [`01-architecture-and-init.md`](01-architecture-and-init.md) — process entry → window → device → frame loop
- [`02-class-hierarchy.md`](02-class-hierarchy.md) — UI class hierarchy + virtual dispatch
- [`03-rendering.md`](03-rendering.md) — font, sprite, text, cursor render pipeline
- [`04-input-events.md`](04-input-events.md) — Win32 WndProc → Device → widget event routing
- [`05-panels.md`](05-panels.md) — chat, attributes, skills, spell, paperdoll, inventory, etc.
- [`06-hud-and-assets.md`](06-hud-and-assets.md) — globes, radar, compass + dat-file UI assets
This document is the combined, cross-referenced picture of how the
retail AC client renders and drives its UI, plus the port plan for
acdream.
---
## 0. TL;DR — single most important finding
**The retail UI widget toolkit is not in `acclient.exe`.** The client
loads `keystone.dll` (plus two plugins `ACHelpPlugin.dll`,
`ACPluginManager.dll`) at startup via `LoadLibraryA`, gets a single
COM-like interface pointer (`DAT_00870c2c`), and delegates all widget
layout, hit-testing, input, and drawing to Keystone. The client
supplies primitives Keystone consumes (`CFont`, `CSurface`, `CString`,
the D3D device) and routes commands by resource ID (`0x10000000+`
custom event IDs, `0x21xxxxxx` LayoutDesc IDs, `0x06xxxxxx` sprites,
`0x40000xxx` fonts).
**Implication for acdream:** we cannot "port a C++ widget hierarchy
from the decompile" — there is none. We implement our own retained-mode
toolkit on top of Silk.NET OpenGL, **consuming the same dat-borne
textures and fonts** so the UI is visually identical to retail even
though the class hierarchy underneath is new. This is the same
approach AC2D took. Section 11 below describes the C# layout.
---
## 1. Process / frame architecture (from slice 01)
```
WinMain @ 0x004013A0 (chunk_00400000.c)
└─ App::Initialize (vtable[0x10])
├─ dat cache init, string-table init
├─ vtable[0x74] "CreateMainWindow"
│ ├─ RegisterClassA("Turbine Device Class")
│ ├─ CreateWindowExA(..) → DAT_008381a4 (HWND)
│ ├─ graphics factory → DAT_0086734c
│ ├─ render device → DAT_00837ff4 (lightweight)
│ ├─ Turbine Core → DAT_00870340 (GL surfaces + dats)
│ └─ Keystone bring-up:
│ LoadLibraryA("keystone.dll") → DAT_00870c30
│ LoadLibraryA("plugins\\ACHelpPlugin.dll")
│ LoadLibraryA("plugins\\ACPluginManager.dll")
│ GetProcAddress "KeystoneCreate" → DAT_00870c34
│ KeystoneCreate(hwnd, core, cwd, 0,0,0,0) → DAT_00870c2c
├─ App::Run (vtable[0x1c]) — main loop forever
│ while (!quit) {
│ PumpMessages @ 0x00439E50 // FUN_00439e50
│ PeekMessageA
│ IME filter (stub)
│ Keystone->TranslateAccelerator (vtable +0x6c)
│ TranslateMessage + DispatchMessageA → WndProc @ 0x00439860
│ RenderFrame @ 0x0045D0B0 // 3D world + UI + overlays
│ GameTick @ 0x004554B0 // physics + AI + scripts
│ FlushPresent @ 0x0043FCD0 // clear + begin + UI + end + Present
│ Sleep(~1ms)
│ }
└─ App::Shutdown (vtable[0x2c])
```
**Frame body ordering (important):** render → tick → Sleep. Retail
renders BEFORE the game tick, because the render pass reads a stable
snapshot built at the end of the previous frame (double-buffered
simulation state). acdream already follows this pattern.
**Global state block (acdream should replicate the 4-way factoring):**
| Retail global | Role | Proposed C# class |
|---------------|-----------------------------|------------------------------------------------|
| `DAT_008381a4` | HWND | (Silk.NET `IWindow`) |
| `DAT_0086734c` | Graphics factory | (handled by Silk.NET GL selection) |
| `DAT_00870340` | Turbine Core (GL + dats + fonts) | `AcDream.App.UI.UiHost` |
| `DAT_00837ff4` | Render/input device | `AcDream.App.UI.UiDevice` |
| `DAT_00870c2c` | Keystone root (widgets) | `AcDream.App.UI.UiRoot` (our toolkit) |
---
## 2. Rendering pipeline (from slice 03)
### 2.1 Top-level frame
```
FUN_0043fcd0 SceneTool::RenderFrame
├─ RenderDevice::Clear(color|depth)
├─ if world.visible:
│ Scene::BeginFrame
│ Scene::RenderWorld (terrain, statics, lights)
├─ if DAT_0083846c != 0: FUN_005da8f0() <-- UI PANEL TREE WALK (7.8 KB func)
├─ if DAT_00838468 != 0: FUN_00692470() <-- CHAT TEXT + EDIT LINE
├─ FUN_0043f7f0() <-- DEBUG/STATS HUD
├─ RenderDevice::Clear(color only)
├─ RenderDevice::BeginScene
├─ RenderDevice::EndScene + Present
└─ FUN_0043e6b0() <-- frame stats
```
### 2.2 Single sprite / text quad
- Vertex format: **24 bytes** = `(x, y, z=0, rhw, u, v)` — XYZRHW + TEX1
- No per-vertex color; color is set via `D3DRS_TEXTUREFACTOR` per draw
- 6 verts per quad (TL, BL, BR, BR, TR, TL — CCW with D3D conventions)
- NDC conversion applies a DX9-style **half-pixel offset**
- Global buffer at `DAT_008f9a90..8f9a98` (base, capacity, write-offset)
- Hot loop unrolled 4 glyphs per iteration
- Flush: `FUN_006974d0``FUN_005a26d0(D3DPT_TRIANGLELIST, verts/3, pV, 0x142, tex, tex, stride=24)`
### 2.3 Fonts
- DBObj type `Font` at IDs `0x40000000..0x40000FFF` (portal.dat)
- On-disk `FontCharDesc` per glyph (9 bytes: codepoint, offsetX/Y, w, h, kern-before/after, vertOffset)
- References a `RenderSurface` (`0x06xxxxxx`) for the source pixel sheet
- Runtime baking (`FUN_00697140`) builds a **256×256 atlas** from the source surface, clamps to printable ASCII 0x20-0x7E (or wider if present), and caches glyph UVs + metrics as 24-byte runtime records
- **No inline color escapes.** Chat colors arrive as full ARGB dwords on the wire in `ChatText`/`ChannelBroadcast` messages
### 2.4 UI textures
| ID range | DBObjType | Role |
|---------------------------|------------------|------|
| `0x06000000..0x06FFFFFF` | RenderSurface | **All** button images, panel backgrounds, icons, cursor bitmaps, font glyph sheets |
| `0x08000000..0x08FFFFFF` | SurfaceTexture | Wraps a RenderSurface with palette/tiling info |
| `0x0F000000..0x0FFFFFFF` | SurfaceMaterial | Materials referencing SurfaceTextures |
| `0x40000000..0x40000FFF` | Font | Font metadata — references a RenderSurface |
| `0x21xxxxxx` | LayoutDesc | **Panel layouts** — tree of ElementDescs + StateDescs |
| `0x23/0x24xxxxxx` | StringTable | Localized UI strings |
| `0x14xxxxxx` | MasterInputMap | Default keybinds |
| `0x41xxxxxx` | LanguageInfo | Localization root |
### 2.5 Blend + depth + clipping
- Blend: `SRC_ALPHA / INV_SRC_ALPHA` ("over")
- Depth: off for UI
- Clipping: retail mostly pre-clips at the vertex-generation stage; true hardware scissor is used in some panel interiors (chat scroll, inventory grid)
---
## 3. Class hierarchy / polymorphism (from slice 02)
### 3.1 What is in acclient.exe
| Class | Vtable | Role |
|---------------------------|--------------------------|------------------------------------------|
| `CUIManager` (singleton) | `PTR_FUN_00799fc4` | `DAT_00838374`; AddListener/RemoveListener at vtable +0x0c |
| `CUIListener` (embedded base) | `PTR_FUN_00801670` | Any object wanting UI events embeds this as MI secondary base |
| `CFont` (chunk_00440000.c) | — | Glyph lookup, range map, atlas blit |
| `CSurface` | `PTR_FUN_0079c26c` | 40-byte bitmap wrapper (w, h, bpp, pixels) |
| `CString` helpers | `FUN_00402490/07e40/0b8f0` | Refcounted wide-string buffer (string builder) |
### 3.2 The Keystone interface (`DAT_00870c2c`)
Observed vtable slots (from call sites in `acclient.exe`):
| Slot | Role |
|-------|---------------------------------------------------------------|
| +0x08 | Release / destructor |
| +0x14 | FindPlugin(wchar_t* name) |
| +0x20 | ProcessFrame — called once per frame from `FUN_0043fcd0` |
| +0x24 | CreateOrActivate(a, b, c, d) — 4-arg plugin entry |
| +0x28 | ClearActive(0) |
| +0x2c | GetActive |
| +0x5c | SendCommand(id, op, buffer) — generic command dispatch |
| +0x60 | HitTest(POINT*) |
| +0x6c | TranslateAccelerator(hwnd, haccel, &MSG) — pump's pre-filter |
### 3.3 Polymorphism pattern
Classic MSVC **multiple inheritance with per-base-class vtables**. A
widget like CharGen has four vtables at fixed offsets inside the
object (`+0x00`, `+0x04`, `+0x08`, `+0x12C`). AddListener is passed
the INNER `this` pointer (the listener subobject), not the outer.
### 3.4 Port decision
**acdream implements its own retained-mode widget toolkit.** Keystone
is not decompiled here and reverse-engineering it is out of scope. We
build `UiElement` / `UiPanel` / `UiButton` / `UiLabel` / `UiEditBox` /
etc. with virtual `OnDraw` / `OnHitTest` / `OnMouseMessage` /
`OnKeyMessage`**not** multiple inheritance.
---
## 4. Input / event routing (from slice 04)
### 4.1 Message pump
```c
char FUN_00439e50(void) { // 0x00439E50
DAT_00838198 = 1;
MSG msg;
while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE) && msg.message != WM_QUIT) {
if (FUN_006a1050(&msg)) continue; // IME filter (stub)
if (FUN_00557a90(msg.hwnd, 0, &msg)) continue; // Keystone accel
TranslateMessage(&msg);
DispatchMessageA(&msg); // → WndProc at 0x00439860
}
DAT_00838198 = 0;
return DAT_00838194; // quit flag
}
```
### 4.2 WndProc
Ghidra did not decompile `LAB_00439860` but its behavior is
reconstructed from dispatch sites:
- Win32 WM_* numbers are **reused as internal event type codes**
(`0x200` mouse-move, `0x201` LDown, `0x202` LUp, `0x204/205` RDown/Up, etc.)
- All WM_* forwarded to `DAT_00837ff4` (the Device)'s vtable methods
### 4.3 Device object (`DAT_00837ff4`) vtable
| Slot | Method |
|--------|-------------------------------------------------------------|
| +0x04 | AttachWindow(HWND) |
| +0x10 | BeginScene |
| +0x18 | GetMouseX |
| +0x1C | GetMouseY |
| +0x34 | RegisterTimerEvent(eventType, target, delayMs) — tooltip delays |
| +0x38 | FireEvent(eventType, target) |
| +0x44 | GetKeyboardFocus |
| +0x48 | SetCapture(widget, kind) |
| +0x4C | ReleaseCapture(kind) |
| +0x6C | TranslateAccelerator |
| +0x70 | WndProc filter — `(MSG*, bool* handled)` |
| +0x74 | SetDragCursor(bool) |
| +0x78 | ResetDragDrop |
### 4.4 Event struct — delivered to widget vtable +0x128
```c
struct Event {
int source_id; // widget event id (0x10000000+ app range)
Widget* target_widget;
int event_type; // see table below
int data0, data1, data2, data3;
}
```
Widget vtable +0x128 = `OnEvent(Event*)`.
### 4.5 Event type catalog
| Code | Meaning | Notes |
|--------|-------------------------------|-------|
| 0x01 | Click (left-up-over) | |
| 0x05 | Hover enter | |
| 0x06 | Hover leave | |
| 0x07 | Tooltip (delayed, ~1000ms) | Target registers via Device::RegisterTimerEvent(7, self, ms) |
| 0x08 | Double-click | |
| 0x0A | Scroll-wheel | data0 = wheel delta |
| 0x0E | Right-click | |
| 0x15 | Drag begin | Sent to source on motion > ~3px |
| 0x1C | Drag-over (continuous) | |
| 0x21 | Drag entered target | |
| 0x3E | Drop released | data3 = accepted? |
| 0x201 | Mouse button down (raw) | |
| 0x202 | Mouse button up (raw) | |
| 0x200 | Mouse move | |
| 0x100 | Key down | data0 = VK |
| 0x101 | Key up | |
| 0x102 | Char typed | |
### 4.6 Drag-drop
Single-drag-at-a-time. The Device holds `DragSource` + `DragPayload`;
the event struct carries only IDs. Targets ask the Device for the
current payload. Drag threshold ~3px from button-down to promote.
### 4.7 Modality
One `modal_overlay` slot on the Device. Clicks outside the modal's
rect are dropped. All other panels are drawn dimmed (gray quad
underneath). Login screen is the first modal.
### 4.8 Focus / hotkeys
**No Win32 accelerator table.** CreateAcceleratorTableA called with
empty table. All hotkeys done in-widget:
- Focus widget (or top panel if no focus) gets first shot at WM_KEYDOWN
- If not consumed, walk top-level panels in z-order
- Finally, global hotkeys (1-0 quickbar, B/I/C panel-open, etc.)
- Chat-typing mode checks focus: if focus is in EditBox, suppress hotkeys
---
## 5. Panel inventory (from slice 05)
Summarized mapping of retail panels to the decompiled code:
| Panel | Chunk | Key function / evidence |
|-----------------------|--------------------------|--------------------------------------------------------|
| Chat window | chunk_00570000.c | Channel bitmask + ChatMessageType 0x00-0x1F + 7 wire opcodes |
| Character Attributes | chunk_00470000.c | `FUN_0047ba70` — 10 attribute rows, 4 skill categories |
| Skills | (same as Attributes) | Embedded in attribute sheet |
| Spellbar / Spellbook | chunk_004C0000.c | `FUN_004C7700-004C7F00` — 8 tabs × 7 slots |
| Paperdoll / Equipment | chunk_004A0000.c | `FUN_004A5200` — 25 slots at offsets +0x604..+0x664 |
| Inventory | chunk_004A0000/05C0000 | Burden system + error string catalog |
| Quickbar | (light coverage) | 1-10 hotkey slots |
| Fellowship | (GameEventFellowshipFullUpdate) | Member list + XP share |
| Allegiance | (Allegiance messages) | Patron/vassal tree |
| Character Select | (Login flow) | Character slots + create-new |
| Login | — | First modal |
| Options | chunk_00400000.c (partial) | 55 CharacterOption bits |
### 5.1 Chat channels (32-bit bitmask)
Full channel enum: Abuse (0x1), Admin (0x2), Audit (0x4), Advocate1-3,
QA1/2, Debug, Sentinel, Help, **Fellow (0x800), Vassals (0x1000),
Patron (0x2000), Monarch (0x4000)**, 8 Society town channels, CoVassals,
AllegianceBroadcast, FellowBroadcast, 3 Society groups, Olthoi.
### 5.2 Chat message types (color / filter enum)
0x00 Broadcast (default), 0x02 Speech, 0x03 Tell, 0x04 OutgoingTell,
0x05 System (red), 0x06 Combat, 0x07 Magic, 0x08 Channel (pink), 0x0A
Social (yellow), 0x0C Emote, 0x0D Advancement, 0x0E Abuse, 0x12
Allegiance, 0x13 Fellowship, 0x14 WorldBroadcast (green), 0x15
CombatEnemy (red), 0x16 CombatSelf (pink), 0x18 Craft, 0x19 Salvaging
(green), 0x1F AdminTell.
### 5.3 Paperdoll slot offsets
25 equipment slots at offsets `+0x604` through `+0x664` (4 bytes
apart) in the paperdoll panel's `this`. Correlated with the EquipMask
bit layout.
### 5.4 Cross-panel primitives identified
These recur across multiple panels and should be extracted as shared
C# helpers:
- `WidgetLookup` — lookup by resource ID (`FUN_00463c00(0x1000xxxx)`)
- `RichTextBuilder` — stream-append wide-string + format tokens
- `TooltipBuilder``FUN_0042dc80` open → content → `FUN_0042e590` close
- `DragDropRouter` — routes 0x15 / 0x1C / 0x21 / 0x3E chains
- `RowList<T>` — used by skills, spells, fellowship members, allegiance tree
- `PropertyUpdateBus` — dispatches `GameMessagePrivateUpdate*` into bound widgets
---
## 6. HUD + dat-asset catalog (from slice 06)
### 6.1 HUD elements
| Element | Rendering technique | Retail dat IDs (from AC2D) |
|---------------------|--------------------------------------------------------------------|----------------------------|
| Vital orbs (3) | Textured quad + **scissor rect** for fill fraction; not gradients | `0x060013B2` (vitals icon) |
| Radar | Polar plot with blips; `1.18 * range` shrink factor; player arrow center | `0x06001388`, `0x06004CC1` (bezel) |
| Compass strip | Scrolling horizontal texture; U-offset = `heading_deg / 360` | — |
| Minimap | Larger radar variant | `0x06001065` |
| Quickbar | 10 slots; hotkey = 1-0 | `0x06001AB2` (slot) |
| Target health bar | Plate appears over selected creature | — |
| Damage floaters | Text rising above head for N seconds | — |
| Announcement strip | Center-top for big events | — |
| World hover name | Text over a 3D object under the cursor | — |
### 6.2 UI dat type catalog (authoritative; verified from chunk_00410000.c range dispatcher)
| ID range | DBObjType | Size | Role |
|---------------------------|-------------------|------|------|
| `0x04xxxxxx` | Palette | var | 256-entry BGRA8 palettes |
| `0x05xxxxxx` | PaletteSet | var | Tintable palette groups |
| `0x06xxxxxx..0x07xxxxxx` | RenderSurface | var | All UI images (buttons, icons, backgrounds, glyph sheets, cursors) |
| `0x08xxxxxx` | Surface | var | Extended surface with palette ref |
| `0x0F000000..0x0FFFFFFF` | SurfaceMaterial | var | Materials |
| `0x14xxxxxx` | MasterInputMap | var | Default keybind table |
| `0x21xxxxxx` | LayoutDesc | var | **Panel layouts** (tree of ElementDesc + StateDesc + Media) |
| `0x23-0x24xxxxxx` | StringTable | var | Localized strings |
| `0x40000000..0x40000FFF` | Font | var | Font metadata + glyph records |
| `0x41xxxxxx` | LanguageInfo | var | Localization root |
### 6.3 Vital update wire messages
- `GameMessagePrivateUpdateAttribute` (0x02E3) — 21 bytes per attribute
- `GameMessagePrivateUpdateVital` (0x02E7) — 25 bytes
- `GameMessagePrivateUpdateAttribute2ndLevel` (0x02E9) — max-value refresh
- `GameEventPlayerDescription` (0xF7B0/0x0013) — initial full dump at login
VitalId enum: 0x01 MaxHealth, 0x03 MaxStamina, 0x05 MaxMana
CurVitalId enum: 0x02 CurHealth, 0x04 CurStamina, 0x06 CurMana
### 6.4 Cursor
- Default: `LoadCursorA(NULL, IDC_ARROW=0x7F00)`
- Custom cursors: built from RGBA dat bitmap via GDI path
(`FUN_0043c1c0` / `FUN_00439c70`) → `HCURSOR` via `CreateIconIndirect`
- Rendering mode: OS cursor suppressed when UI draws its own sprite
- `SetCursorPos` used in mouse-look mode to recenter
---
## 7. Proposed C# port structure
### 7.1 Namespace + project layout
```
src/AcDream.App/UI/
UiRoot.cs // top-of-tree, implements IDevice, called from GameWindow
UiElement.cs // base widget class
UiPanel.cs // container with children
UiLabel.cs // text
UiButton.cs // clickable
UiEditBox.cs // text entry
UiScrollBar.cs
UiList.cs // scrollable list
UiImage.cs // sprite
UiWindow.cs // movable / closable frame
AcFont.cs // dat-sourced font (portal.dat Font DBObj)
FontCache.cs // lazy-load font atlases
UiSpriteBatch.cs // 2D batched quad renderer (replaces our debug TextRenderer)
UiRenderer.cs // per-frame UI orchestrator — walks tree, emits draws
UiEvent.cs // event struct matching retail's 24-byte payload
CursorManager.cs // OS + dat cursor management
src/AcDream.App/UI/Panels/
ChatWindow.cs
AttributesPanel.cs
SkillsPanel.cs
SpellPanel.cs
PaperdollPanel.cs
InventoryPanel.cs
QuickbarPanel.cs
FellowshipPanel.cs
AllegiancePanel.cs
LoginPanel.cs
CharacterSelectPanel.cs
src/AcDream.App/UI/Hud/
VitalOrbs.cs
RadarPanel.cs
CompassStrip.cs
SelectionTarget.cs
DamageFloaters.cs
AnnouncementStrip.cs
WorldHoverName.cs
```
### 7.2 Scaffold to ship in this session
Minimum viable scaffold matching the research findings:
1. `UiEvent` struct + event type enum (24 bytes, matches retail)
2. `UiElement` abstract base with virtual Draw/OnMouse/OnKey/OnTick/OnHitTest + children list
3. `UiPanel` container that composites children
4. `UiRoot` that implements `IDevice` semantics (focus, modal, capture,
drag) and can be driven by `GameWindow`'s existing Silk.NET input
handlers
5. `UiSpriteBatch` — derived from our existing `TextRenderer` but with
scissor support added
6. `UiRenderer` — per-frame entry point, walks the tree
7. Integration stub in `GameWindow.cs` — creates `UiRoot`, routes
Silk.NET mouse/keyboard to it with "world fall-through" for any
unconsumed event
Later (follow-up sessions):
- `AcFont` from portal.dat Font DBObj (slice 3 has exact on-disk layout)
- `CursorManager` for dat-cursor support
- First real panel — chat window — since we have wire messages ready
- Vital orbs once server sends `GameMessagePrivateUpdateVital`
### 7.3 Event type mapping to Silk.NET input
| Silk.NET event | UiEvent type |
|---------------------------------|-----------------------------|
| IMouse.MouseDown (Left) | 0x201 |
| IMouse.MouseUp (Left) | 0x202 |
| IMouse.MouseMove | 0x200 |
| IMouse.Scroll | 0x0A |
| IKeyboard.KeyDown | 0x100 |
| IKeyboard.KeyUp | 0x101 |
| IKeyboard.KeyChar | 0x102 |
| (synthesized on hover-delay) | 0x07 (tooltip) |
| (synthesized on drag distance) | 0x15/0x1C/0x21/0x3E drag-chain |
| (synthesized by UiRoot) | 0x05/0x06 hover enter/leave |
### 7.4 Key constants to preserve
Using retail's magic numbers lets hand-ported panel code (e.g., a
future port of `FUN_0047ba70``AttributesPanel`) use the same
event_type switches the original uses:
```csharp
public static class UiEventType
{
public const int Click = 0x01;
public const int HoverEnter = 0x05;
public const int HoverLeave = 0x06;
public const int Tooltip = 0x07;
public const int DoubleClick = 0x08;
public const int Scroll = 0x0A;
public const int RightClick = 0x0E;
public const int DragBegin = 0x15;
public const int DragOver = 0x1C;
public const int DragEnter = 0x21;
public const int DropReleased = 0x3E;
public const int MouseDown = 0x201;
public const int MouseUp = 0x202;
public const int MouseMove = 0x200;
public const int KeyDown = 0x100;
public const int KeyUp = 0x101;
public const int Char = 0x102;
}
```
### 7.5 Integration into existing `GameWindow.cs`
Current state: `GameWindow` binds Silk.NET `IInputContext` directly to
`PlayerMovementController` + camera. We insert `UiRoot` between:
```csharp
// In OnLoad:
_uiRoot = new UiRoot();
_uiRoot.WorldMouseFallThrough += (button, x, y, flags) => { /* existing camera/world handling */ };
_uiRoot.WorldKeyFallThrough += (vk, lp) => { /* existing camera/world hotkeys */ };
mouse.MouseDown += (_, b) => _uiRoot.OnMouseDown(MapButton(b), (int)m.Position.X, (int)m.Position.Y, 0);
// similar for MouseUp, MouseMove, Scroll, KeyDown, KeyUp, KeyChar
```
UiRoot dispatches to focused widget / modal / top panel first; if no
widget consumes the event, the `WorldMouseFallThrough` /
`WorldKeyFallThrough` events fire, preserving the existing camera
controls (sensitivity, RMB orbit, etc.).
### 7.6 Render integration
In `GameWindow.OnRender`, after the 3D scene and before the debug HUD:
```csharp
// 3D world (existing)
_terrain?.Draw(camera, frustum);
_staticMesh?.Draw(...);
_debugLines?.Flush(...);
// Retail UI pass
_uiRenderer.BeginFrame(screenSize);
_uiRoot.Draw(_uiRenderer);
_uiRenderer.EndFrame(); // flushes sprite batches
// Existing debug overlay (dev HUD)
_debugOverlay?.Draw(...);
```
---
## 8. What we do NOT port
### 8.1 Keystone itself
`keystone.dll` is binary-only and not decompiled. We do not attempt to
match its internal API. Our widget toolkit is analogous but not byte-
compatible.
### 8.2 XML layout loader
Retail stores panel layouts as MSXML4-parsed XML. Our panels are
code-defined for the MVP. If we ever want to consume the retail
`.dat`-embedded layouts directly, we write an XML->widget-tree
deserializer in a later phase.
### 8.3 ACHelpPlugin's embedded browser
The help/tutorial pane is a hosted Internet Explorer control
(`CoCreateInstance(CLSID_007cc680)`). We ship our own help pane (flat
text or a WebView2 embed if we ever need HTML).
### 8.4 IME composition
The production build has the IME hook stubbed. We don't target CJK
localizations for the MVP.
---
## 9. Risk + sequencing
### 9.1 What can ship next session (low risk)
1. `UiElement` + `UiPanel` + `UiRoot` + `UiSpriteBatch` scaffold (this session)
2. Port `BitmapFont``AcFont` from portal.dat `Font` DBObj
3. Chat window panel (wire messages already parsed by `WorldSession`)
### 9.2 Medium risk
4. Vital orbs + radar (needs server `PrivateUpdateVital` wiring)
5. Inventory panel (needs item CreateObject parsing + icon lookup)
6. Drag-drop plumbing (requires tooltip timer + capture + payload state)
### 9.3 High risk
7. Character creation screen (needs the whole Attribute/Skill model)
8. Trade window (multi-player state, confirms)
9. Spellcasting UI (component consumption, formula display, cast progress)
---
## 10. Lookup table: retail function → proposed C# method
Only the high-confidence mappings. Many more in the slice docs.
| Retail address | Role | Proposed C# class.method |
|----------------|---------------------------|-----------------------------------------|
| `0x004013A0` | WinMain | `Program.Main` |
| `0x0043BA60` | CreateMainWindow | `GameWindow.OnLoad` |
| `0x00439E50` | PumpMessages | (Silk.NET internal) |
| `0x00439860` | WndProc | (Silk.NET internal; our UiRoot handles) |
| `0x0045D0B0` | RenderFrame | `GameWindow.OnRender` |
| `0x004554B0` | GameTick | `GameWindow.OnUpdate` |
| `0x0043FCD0` | Flush+Present | (Silk.NET internal) |
| `0x005DA8F0` | UI tree walker | `UiRenderer.Draw(UiRoot)` |
| `0x00697140` | Font load+bake | `AcFont` constructor |
| `0x00697770` | Text→quads | `UiSpriteBatch.DrawString` |
| `0x006974D0` | Flush text | `UiSpriteBatch.Flush` |
| `0x0043DCD0` | px→NDC | `UiSpriteBatch` private helper |
| `0x0043C1C0` | HCURSOR from bitmap | `CursorManager.SetFromDat` |
| `0x00557850` | KeystoneCreate call | `UiRoot` constructor |
| `0x00692470` | Chat panel render | `ChatWindow.Draw` |
| `0x0047BA70` | Attributes panel render | `AttributesPanel.Draw` |
| `0x004A5200` | Paperdoll slot tooltip | `PaperdollPanel.BuildSlotTooltip` |
---
## 11. Summary — what this pass ships today
- **Full research** (this synthesis + 6 detailed slice docs)
- **C# port scaffold** (sections 7.1-7.6):
- `UiEvent`, `UiEventType`, `UiElement`, `UiPanel`, `UiRoot`, `UiSpriteBatch`
- Integration stub in `GameWindow` — hooks Silk.NET events through
UiRoot with world fall-through
- **Updated architecture doc**`docs/architecture/acdream-architecture.md`
gets a new UI-layer section pointing at the synthesis
- **Roadmap** — new phase **U1 Retail UI Framework** appended
Not shipped this session (scheduled for follow-up):
- Font loading from portal.dat (AcFont from Font DBObj)
- CursorManager with dat-sourced custom cursors
- Any concrete panel (chat / attributes / etc.)
- Vital orb HUD
- Scissor clip support in UiSpriteBatch
---
## 12. Open questions
1. **Widget IDs source.** The hardcoded `0x1000001c`, `0x100002fc`
resource IDs — are these in `keystone.dll`'s layout tables, or in
`0x21xxxxxx` LayoutDesc dats? Likely dats; confirm by dumping one
LayoutDesc and looking for these IDs inside.
2. **Tooltip delay.** Retail registers the timer with some constant ms;
grep for `0x3e8` (1000) or `0x1f4` (500) near
`RegisterTimerEvent(7, …)`.
3. **Chat protocol gaps.** We know 6 wire messages drive chat; need
to verify tab-filter persistence (is the "General" tab's filter
mask stored server-side or client-side?).
4. **Inventory weight warnings.** The error string catalog
mentioned in slice 05 needs full enumeration when we port
the inventory panel.
5. **Quickbar persistence.** Server-side or saved to `.ini`?

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff