Landed the UI framework design in 2026-04-24-ui-framework.md yesterday;
this commit propagates the decisions across the documents that future
sessions touch first, so the three-layer pattern is discoverable without
re-reading the full plan.
Changes:
* NEW memory/project_ui_architecture.md — evergreen crib-sheet:
three-layer diagram, AcDream.UI.Abstractions contract, D.2a/D.2b
split, module layout, hard rules, why staged not pure-custom.
* CLAUDE.md: new paragraph describing the three-layer UI split, naming
AcDream.UI.Abstractions as the plugin-facing contract, pointing at
the full plan + memory crib.
* docs/architecture/acdream-architecture.md: new "UI Architecture"
companion-stack diagram after Layer 0-5 (doesn't renumber the main
stack), plus step 6a "UI tick" in Per-Frame Update Order.
* docs/plans/2026-04-11-roadmap.md Phase D tightened:
- D.2 split explicitly into D.2a (Hexa.NET.ImGui scaffold + abstraction
layer) and D.2b (custom retail-look backend, implements same contracts).
- D.3 AcFont / D.4 dat sprites / D.7 cursor flagged as D.2b dependencies.
- D.5 core panels / D.6 HUD flagged as abstraction-layer deliverables
— ship with D.2a, reskinned by D.2b.
- D.8 Sound marked superseded (shipped as Phase E.2).
- F.5 core panels + H.1 chat-window cross-references updated to say
they target AcDream.UI.Abstractions, unblocked by D.2a.
- Shipped-phases table untouched.
* docs/research/retail-ui/00-master-synthesis.md: scope note at top
clarifies the Keystone research is the D.2b (custom backend)
foundation, NOT where D.2a starts.
* ~/.claude/.../memory/MEMORY.md: one-line index entry pointing at the
new project_ui_architecture.md (so session auto-load surfaces it).
Zero code changes; doc-only. dotnet build stays green. All verification
greps pass (see plan file for exact checks).
682 lines
31 KiB
Markdown
682 lines
31 KiB
Markdown
# Retail AC Client GUI — Master Synthesis
|
||
|
||
> **Scope note (2026-04-24):** 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 (Hexa.NET.ImGui scaffold)**.
|
||
> 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`?
|