# Retail AC Client GUI — Master Synthesis

> **Scope note (2026-04-24, updated 2026-04-25):** This document
> describes retail's Keystone UI toolkit — it is the research foundation
> for **Phase D.2b (custom retail-look backend)**, not Phase D.2a
> (shipped ImGui scaffold, `AcDream.UI.Abstractions` + ImGui.NET +
> `Silk.NET.OpenGL.Extensions.ImGui` + `VitalsPanel`). When reading this
> for implementation guidance, assume D.2a has shipped a working
> `AcDream.UI.Abstractions` layer (`IPanel`, `IPanelRenderer`,
> ViewModels, Commands) and you are building the custom retained-mode
> toolkit that implements the same contracts using dat-sourced fonts /
> sprites / cursors. See `docs/plans/2026-04-24-ui-framework.md` for the
> staged UI strategy and `memory/project_ui_architecture.md` for the
> one-page crib-sheet.

**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`?
