Closes Phase D.2a. Launch with ACDREAM_DEVTOOLS=1 now shows a live
ImGui "Vitals" window whose HP bar reads CombatState.GetHealthPercent
for the local player. Without the env var the branches are dead code,
no ImGui context is created, and behaviour is identical to before.
GameWindow hunks:
- fields: _imguiBootstrap / _panelHost / _vitalsVm + DevToolsEnabled
- init (OnLoad): construct bootstrap + host, register VitalsPanel
- GUID push: _vitalsVm?.SetLocalPlayerGuid(chosen.Id) at live-connect
- frame begin: _imguiBootstrap.BeginFrame(dt) after GL clear
- frame end: _panelHost.RenderAll(ctx) + _imguiBootstrap.Render() after debug overlay
- input gating: skip WASD when ImGui.GetIO().WantCaptureKeyboard
Backend pivot: Hexa.NET.ImGui → ImGui.NET + Silk.NET.OpenGL.Extensions.ImGui.
First-light integration with the Hexa backend crashed 0xC0000005 inside
Hexa.NET.ImGui.Backends.OpenGL3.ImGuiImplOpenGL3.InitNative. Root cause:
Hexa's native OpenGL3 backend resolves GL function pointers via GLFW or
SDL internally; with Silk.NET (which uses neither) the pointers are null
and the native code crashes on first use. The mitigation path was
already planned — the design doc's Risk section called a pivot to
ImGui.NET a "one-morning operation" — and that's exactly what happened.
- Packages: Hexa.NET.ImGui 2.2.9 + Hexa.NET.ImGui.Backends 1.0.18
→ ImGui.NET 1.91.6.1 + Silk.NET.OpenGL.Extensions.ImGui 2.23.0
- ImGuiBootstrapper: was static Initialize(gl)+Shutdown() wrapping
Hexa's OpenGL3 init; now an IDisposable wrapping Silk.NET's
ImGuiController instance which handles GL backend init + input
subscription in one go.
- SilkInputBridge.cs deleted (~190 LOC): ImGuiController subscribes
IKeyboard / IMouse events itself, we don't need a bespoke bridge.
- ImGuiPanelRenderer: ImGuiNET.ImGui.* calls instead of
Hexa.NET.ImGui.ImGui.*. Widget surface unchanged.
Boundary discipline is preserved — no panel imports ImGuiNET; only
ImGuiPanelRenderer does. The D.2b custom toolkit will implement the
same IPanelRenderer contract without touching panel code.
Out of scope (tracked for follow-up):
- Stam/Mana currently return float? null (VitalsVM). Absolute values
need LocalPlayerState + PlayerDescription (0x0013) parsing to be
stored rather than discarded — filed as a post-D.2a issue.
- Mouse-capture gating (WorldMouseFallThrough-style click-through
tests) — not needed until we add clickable inventory items.
Roadmap + memory + architecture doc + UI framework plan updated in the
same commit per CLAUDE.md roadmap-discipline rules. 753 tests pass
(550 Core + 192 Core.Net + 11 new UI.Abstractions), 0 build warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
684 lines
31 KiB
Markdown
684 lines
31 KiB
Markdown
# 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`?
|