# Retail AC UI — Class Hierarchy & Virtual Dispatch **Scope:** map the retail `acclient.exe` UI class hierarchy and polymorphism pattern from the decompiled 688K-line C corpus. Companion to `01-overview.md`, `03-layout.md`, etc. **Executive summary up front (because it inverts the usual assumption):** > **The retail AC client does not have a custom C++ UI widget hierarchy > baked into `acclient.exe`.** The entire interactive UI is delegated to an > external COM/native library called **Keystone** (`keystone.dll`) plus two > plugin DLLs (`plugins\ACHelpPlugin.dll`, `plugins\ACPluginManager.dll`) > that are loaded at startup and talked to through a single interface > pointer `DAT_00870c2c`. Panels like Attributes, Skills, the Paperdoll, > the Spell window, Chat, Login screens, etc. are described to Keystone via > name-based plugin messages and resource IDs; Keystone does layout, > hit-testing, input dispatch, and draw. All of the wide-string label > literals the preflight task flagged (`L"Attributes"`, `L"Strength"`, > `L"Select a spell to cast"`, `L"Drag necklaces here to wear them"`, …) > are **assembled into reference-counted `CString`-style text buffers** by > the client and then handed to Keystone; they are not direct widget > method calls. > > The client-side classes that do exist in `acclient.exe` are > **infrastructure**: a `CString` refcount helper (`FUN_00402490`, > `FUN_00407e40`, `FUN_0040b8f0`), a full `CFont` (`chunk_00440000.c`, with > glyph array + codepoint-range map), a 32-bpp `CSurface`/bitmap > (`PTR_FUN_0079c26c` vtable, 0x28-byte header), a `CKeystoneGlue` that > wraps Keystone's COM pointer, and an event/listener base > (`PTR_FUN_00801670`, with add/remove via a UI-manager singleton > `DAT_00838374`). Everything that looks like a "widget" in other AC > reference repos (AC2D's `cPictureBox`, `cStaticText`, `cEditBox`, > `cScrollBar`, `cMovableWindow`, `cSkillWindow`, …) lives **on the > Keystone side**, not inside `acclient.exe`. Those reference-repo classes > are AC2D's *reimplementation* of what Keystone already did — they are > not what the retail client's compiled code is doing. > > Concretely, this means acdream cannot "port a C# copy of > `CUIElement`" from the decompile — there is no such single class. > acdream has to pick between two paths: **(a) reimplement an equivalent > UI toolkit from scratch on top of our Silk.NET renderer** (the honest > approach, matching AC2D's strategy), or **(b) treat the UI as a > scripted-from-dat layout described in terms of Keystone concepts** > (harder; requires reverse-engineering Keystone's data format and > protocol). Section 11 proposes the C# hierarchy we should adopt for > path (a). The rest of this document is the evidence for that conclusion plus the inventory of primitives that *do* live in `acclient.exe`. --- ## 1. Method and evidence trail I started from the task's input files (`chunk_00470000.c`, `chunk_004A0000.c`, `chunk_004C0000.c`, `chunk_00560000.c`, `chunk_005C0000.c`, `chunk_00430000.c`) and the labeled literals (`L"Attributes"`, `L"Select a spell to cast"`, etc.). I traced those literals through their argument-0 function (`FUN_0040b8f0`), then that function's callee (`FUN_00402490`), then the sibling text-setter (`FUN_00407e40`), then the setter's call sites, then the shared state those call sites read (`DAT_00870340`, `DAT_00870c2c`, `DAT_00838374`, `DAT_0083e72c`, etc.), then the singletons' creator sites (`FUN_00557930`, `FUN_0054d110`, `FUN_0043c640`), then the frame loop at `FUN_0043fcd0` that ties them together. That chain is the map. Address references below are RVAs inside `acclient.exe` as Ghidra labeled them; all `FUN_xxxxxxxx` / `DAT_xxxxxxxx` / `PTR_xxx_yyyyyyyy` symbols are Ghidra's auto-generated names and come directly from `docs/research/decompiled/`. --- ## 2. What I thought would be the UI base class, and why it is not The task's preflight preview identified `FUN_0040b8f0` as "called everywhere with UI labels" (`L"Attributes"`, `L"Strength"`, `L"Mana"`, etc.). That strongly suggested `CUIText::SetText` or `CUILabel::Create` or similar. Disassembly at `chunk_00400000.c:9331` shows the truth: ```c // FUN_0040b8f0 at 0x0040B8F0 (size: 42 bytes) void FUN_0040b8f0(wchar_t *param_1) { if ((param_1 != (wchar_t *)0x0) && (*param_1 != L'\0')) { sVar1 = wcslen(param_1); FUN_00402490(param_1, sVar1); // wcsncpy/refcount append into a // CString target held in ECX } return; } ``` and the sibling `FUN_00407e40` at `chunk_00400000.c:5759`: ```c // FUN_00407e40 at 0x00407E40 (size: 198 bytes) void __thiscall FUN_00407e40(uint *param_1, wchar_t *param_2) { /* CString::operator=(const wchar_t*) */ /* refcount the old buffer, allocate/reuse, wcsncpy param_2 in */ } ``` Both are `CString` operations, not widget calls. The `this` pointer that holds the target buffer is set via `ECX` (thiscall) before the call site and Ghidra's decompiler drops it at the source-line level, which is why the call sites look like `FUN_00407e40(L"Drag necklaces here to wear them")` — in the actual emitted assembly, `ECX` has already been loaded with the pointer to the tooltip/status buffer that the label gets copied into. At `chunk_00430000.c:9041` I see the pattern most cleanly — pasting from the clipboard: ```c GlobalUnlock(hMem); CloseClipboard(); FUN_00407e40(puVar5); // assign pasted wide-string into the chat // input buffer — the buffer is in ECX from the // prior instruction ``` So the "UI labels via `FUN_0040b8f0`" preflight signal is real but misleading: those labels are being baked into string objects that are then fed to the UI layer — not written to widget state directly. The widget layer itself is elsewhere. --- ## 3. Where the UI layer actually is: Keystone Look at `chunk_00550000.c:7027` (`FUN_00557930`, the UI framework bootstrap): ```c undefined4 FUN_00557930(void) // InitKeystoneAndPlugins { if (DAT_00870c30 != (HMODULE)0x0) { return 1; // already loaded } cVar2 = FUN_005577a0(); // feature-enabled check if (cVar2 != '\0') { DAT_00870c30 = LoadLibraryA("keystone.dll"); DAT_00870c34 = GetProcAddress(DAT_00870c30, "KeystoneCreate"); DAT_00870c38 = LoadLibraryA("plugins\\ACHelpPlugin.dll"); DAT_00870c44 = GetProcAddress(DAT_00870c38, "ExecutePlugin"); DAT_00870c48 = GetProcAddress(DAT_00870c38, "TerminatePlugin"); DAT_00870c3c = LoadLibraryA("plugins\\ACPluginManager.dll"); _DAT_00870c4c = GetProcAddress(DAT_00870c3c, "ExecutePlugin"); DAT_00870c50 = GetProcAddress(DAT_00870c3c, "TerminatePlugin"); if (DAT_00870c34 != 0) { return 1; } DAT_00870c54 = CreateAcceleratorTableA((LPACCEL)0x0, 0); } return 0; } ``` And in `FUN_00557850`, the Keystone window instance is created: ```c undefined4 FUN_00557850(void) // CreateKeystoneMainWindow { /* ... */ if ((DAT_00870340 != 0) && (DAT_00870c34 != (code *)0x0)) { /* get cwd, convert UTF-8 -> UTF-16, get the window handle + an HIMC */ DAT_00870c2c = (int *)(*DAT_00870c34)( pHVar1, // HWND (game window) *(undefined4 *)(DAT_00870340 + 0x468), // pointer from graphics dev auStack_4018, // cwd (UTF-16) 0, 0, 0, 0); /* ... */ if (DAT_00870c2c != (int *)0x0) { (**(code **)(*DAT_00870c2c + 0x5c))(0x69, 2, &uStack_6004); return 1; } } return 0; } ``` `DAT_00870c2c` is the **Keystone instance pointer** — a single vtable-backed object the rest of the client talks to. All further UI behavior goes through that pointer's vtable. Every UI method the task asked me to find (hit-test, draw, tick, add child, handle mouse, handle key) is one vtable slot on this object. The client does not implement those; it invokes them. ### 3.1 Observed Keystone vtable slots From cross-referencing the 10+ call sites I can see in chunk_00550000.c, the Keystone instance vtable (offset 0x0 inside the object) looks like: | Slot (byte off) | Callers / purpose | |---|---| | `+0x00` | vtable ptr itself (base-class vfptr) | | `+0x08` | `Release()` / destructor — called at shutdown (`FUN_00557b50`, `FUN_00557e40`) | | `+0x14` | `FindPlugin(const wchar_t *name)` — called with `L"acpluginmanager"` in `FUN_00557d80` | | `+0x20` | `ProcessFrame()` / event-pump — called once per frame in `FUN_00557840`, which is itself called from the main render loop `FUN_0043fcd0` right after the 3D scene is drawn | | `+0x24` | `CreateOrActivate(int,int,int,int)` — 4-arg plugin entry (`FUN_005579f0`) | | `+0x28` | `ClearActive(0)` — `FUN_00557ac0` | | `+0x2c` | `GetActive()` — returns current active panel | | `+0x5c` | `SendCommand(0x69, 2, buffer)` — command-id dispatch | | `+0x60` | `HitTest(POINT *)` — returns active UI object under the point (used by `FUN_005579f0` / input routing) | | `+0x6c` | `TranslateAccelerator(wParam, haccel, lParam)` — keyboard shortcuts (`FUN_00557a90`) | The vtable is not dense — there are gaps in what the client exercises. Slots the client never calls (`+0x04`, `+0x0c`, `+0x10`, `+0x18`, `+0x30`–`+0x58`, `+0x64`, `+0x68`) presumably exist but are only used Keystone-internally. ### 3.2 Plugin side-channel `chunk_00550000.c:7212` (`FUN_00557c50`) shows the plugin bridge: ```c (*DAT_00870c44)(DAT_00870c2c, FUN_00509430, aiStack_a0[0]); /* ACHelpPlugin::ExecutePlugin(keystoneInstance, callback, stringResourceId) */ ``` So panels that the plugin implements (help/tutorial, plugin-manager dialog, etc.) are addressed by resource-ID / string-ID tuples rather than by C++ class pointers. `FUN_00509430` is a client callback that Keystone / the plugin can call back into. This is a clean plugin ABI, not an inheritance hierarchy. ### 3.3 ACHelpPlugin specifically Inside `ACHelpPlugin`, `FUN_00557e80` does a COM `CoCreateInstance(CLSID_007cc680, NULL, CLSCTX_ALL, IID_007cc670, &p)` followed by `QueryInterface(IID_007cc660)`, which is the classic IE-embedded-browser sequence (the CLSIDs match the browser control pattern). So the help/tutorial pane is literally a hosted Internet Explorer control. That is fine for our archival purposes — we do not want to replicate it — but it also means a meaningful chunk of the retail UI is not even in `acclient.exe`, it is just OLE. --- ## 4. The one hierarchy that *does* live in acclient.exe: CKeystoneGlue + listener The only C++ class hierarchy in the binary that matches "something receives UI-ish events" is an internal listener/event-bus pattern tied to the **UI manager singleton** `DAT_00838374`. It is not a widget hierarchy, but it is the closest retail analog. ### 4.1 The UI manager singleton `chunk_00430000.c:10405` (`FUN_0043c640`): ```c undefined4 * __fastcall FUN_0043c640(undefined4 *param_1) { FUN_0043c6c0(); // base-class ctor *param_1 = &PTR_FUN_00799fc4; // derived vtable DAT_00838374 = param_1; // publish singleton return param_1; } ``` And `FUN_0043c680` is the accessor: ```c undefined4 FUN_0043c680(void) { return DAT_00838374; } ``` `DAT_00838374` is accessed through a thunk in many chunks (e.g. `thunk_FUN_0043c680` at `chunk_00470000.c:7709`) which makes the cross-reference pattern obvious: any file that mentions `FUN_0043c680` is consuming the UI/event manager. ### 4.2 The listener base class The listener base has vtable `PTR_FUN_00801670`. Any "object that wants to be notified of UI events" embeds it. See `FUN_0043c610`: ```c void __fastcall FUN_0043c610(undefined4 *param_1) { int *piVar1; *param_1 = &PTR_FUN_00801670; // take on the listener vtable piVar1 = (int *)FUN_0043c680(); // DAT_00838374 (the UI manager) if (piVar1 != (int *)0x0) { (**(code **)(*piVar1 + 0xc))(param_1); // UIManager->AddListener(this) } if (DAT_00842adc != 0) { FUN_00508980(param_1); // also register with world sim? } } ``` And the symmetric remove helper is `(**(code **)(*piVar2 + 0xc))(param_1)` at many sites; vtable slot `+0x0c` on the UI manager is clearly the add/remove listener entry point. The UI manager vtable also exposes slot `+0x04` (from matching destroy patterns) for listener removal. ### 4.3 Multiple-inheritance is the polymorphism pattern The client uses **C++ multiple inheritance with per-base-class vtables** at distinct byte offsets inside a single object. This is the normal Microsoft C++ MI layout. It is visible directly in the CharGen panel constructor at `chunk_00470000.c:7828` (`FUN_0047aa10`): ```c undefined4 * __fastcall FUN_0047aa10(undefined4 *param_1) { undefined4 *puVar1; int *piVar2; undefined4 *puStack_4; puStack_4 = param_1; FUN_004799c0(); // base init param_1[0x4b] = &PTR_FUN_007ccb60; puVar1 = param_1 + 0x4b; *param_1 = &PTR_LAB_0079f870; // vtable #1 — main class param_1[1] = &PTR_FUN_0079f810; // vtable #2 — second base param_1[2] = &PTR_FUN_0079f7f8; // vtable #3 — third base *puVar1 = &PTR_FUN_0079f550; // vtable #4 — listener base, at +0x12c /* ... bunch of ID registrations through thunk_FUN_0043c680 ... */ piVar2 = (int *)FUN_0043c680(); if (piVar2 != (int *)0x0) { (**(code **)(*piVar2 + 4))(0x186a1, puVar1); (**(code **)(*piVar2 + 4))(100000, puVar1); /* 0x186a0 */ if (DAT_00837ff4 != (int *)0x0) { (**(code **)(*DAT_00837ff4 + 0x34))(0xe, param_1 + 2, 4000); } } return param_1; } ``` Four vtables, four base classes, all compiled into the same object: * `PTR_LAB_0079f870` — primary class vtable (the CharGen panel itself) * `PTR_FUN_0079f810` — second base (likely the "screen" / state-machine base) * `PTR_FUN_0079f7f8` — third base (likely a "listener" or "observer" interface) * `PTR_FUN_0079f550` — fourth base, living at `+0x12c` bytes inside the object (stored at `param_1 + 0x4b` which is `+0x12c` bytes) — this is the UI-manager listener base The registration call right after (`(**(code **)(*piVar2 + 4))(0x186a1, puVar1)`) passes the *inner* vtable pointer `puVar1`, not the *outer* `param_1`. That is the Microsoft MI trick: UI-manager code only knows how to cast against the listener base; it gets handed a pointer that *is* the listener subobject's `this`, with the correct adjustment already applied. Equivalent slimmed-down constructor at `chunk_00470000.c:7954` (`FUN_0047b030`) has two vtables, not four, but the same pattern: ```c param_1[0x17e] = &PTR_FUN_007ccb60; // transient / placeholder /* ... fields reset ... */ *param_1 = &PTR_FUN_007a0080; // main class param_1[0x17e] = &PTR_FUN_0079fdd8; // listener base at +0x5f8 ``` and the destructor pairs them in reverse, detaching from the UI manager through the listener vtable before the main destructor runs (`FUN_0047b160`): ```c *param_1 = &PTR_FUN_007a0080; *puVar1 = &PTR_FUN_0079fdd8; piVar2 = (int *)FUN_0043c680(); if (piVar2 != (int *)0x0) { (**(code **)(*piVar2 + 0xc))(puVar1); // UIManager->RemoveListener } *puVar1 = &PTR_FUN_007ccb60; FUN_0043c610(); FUN_004726c0(); // base dtor ``` ### 4.4 Vtables I can attribute with confidence | Vtable symbol | Role | Evidence | |---|---|---| | `PTR_FUN_00799fc4` | UI manager class (singleton at `DAT_00838374`) vtable | published in `FUN_0043c640`; has AddListener at `+0xc`, RemoveListener at `+0xc` variants | | `PTR_FUN_00801670` | Listener base (embedded as secondary base) | assigned in `FUN_0043c610` which then calls UIManager->AddListener | | `PTR_FUN_0079f550` | CharGen-screen listener subobject vtable | inner vtable written alongside CharGen construction, registered under IDs 0x186a1 / 100000 | | `PTR_LAB_0079f870` | CharGen main-class vtable | primary vtable of the big CharGen object (`FUN_0047aa10`) | | `PTR_FUN_0079f810` | CharGen second base (likely state-machine) | second vtable of CharGen | | `PTR_FUN_0079f7f8` | CharGen third base | third vtable of CharGen | | `PTR_FUN_007a0080` | CharGen screen derived-class vtable | `FUN_0047b030` pairs it with listener `PTR_FUN_0079fdd8` | | `PTR_FUN_0079fdd8` | CharGen screen listener subobject vtable | paired with `007a0080` | | `PTR_FUN_007ccb60` | Transient "scratch" vtable used during ctor/dtor in place of the real listener vtable | appears before and after the real listener vtable is installed, probably the plain base's vtable used to avoid calling into uninitialised / torn-down overrides | | `PTR_FUN_0079c26c` | `CSurface` / `CBitmap` vtable (0x28-byte header, byte BPP field, pixels ptr) | constructor at `FUN_0044cdf0` + `FUN_0044cc60`, resize at `FUN_0044ccc0` calls vtable slot `+0x04` with `(width, bpp, ?, ?, ?)` | This is the piece of the hierarchy that *does* exist in `acclient.exe`. It is not a widget hierarchy; it is an event/listener infrastructure that character-generation screens and similar client- side UI moments plug into. --- ## 5. The 40-byte `CSurface` object (`PTR_FUN_0079c26c`) The one clearly delineated "graphics primitive" class I can identify in the binary. Relevant ctors: `FUN_0044cc60`, `FUN_0044cdf0`, `FUN_0054d2a0`. ```c undefined4 * FUN_0044cc60(void) { undefined4 *puVar1; if (DAT_0086734c != 0) { /* WARNING: Could not recover jumptable at 0x0044cc75. Too many branches */ puVar1 = (undefined4 *)(**(code **)(*DAT_00870340 + 0x18))(); return puVar1; /* ask the graphics device for a pooled surface */ } puVar1 = (undefined4 *)FUN_005df0f5(0x28); /* 40-byte alloc */ if (puVar1 != (undefined4 *)0x0) { puVar1[1] = 0; /* +0x04 width */ puVar1[2] = 0; /* +0x08 height */ *(byte *)(puVar1 + 3) = 2; /* +0x0c bytes-per-pixel */ puVar1[4] = 0; /* +0x10 pixel buffer */ *(byte *)(puVar1 + 5) = 0; /* +0x14 byte flag */ *(byte *)((int)puVar1 + 0x15) = 0; /* +0x15 */ *(byte *)((int)puVar1 + 0x16) = 0; /* +0x16 dirty flag */ *(byte *)((int)puVar1 + 0x17) = 0; /* +0x17 */ puVar1[7] = 0; /* +0x1c */ puVar1[8] = 0; /* +0x20 */ *(byte *)(puVar1 + 9) = 0; /* +0x24 byte flag */ *puVar1 = &PTR_FUN_0079c26c; /* +0x00 vtable */ *(byte *)(puVar1 + 3) = 2; /* 2 = default BPP (16-bit??) */ *(byte *)(puVar1 + 6) = 1; /* +0x18 owns-buffer flag */ return puVar1; } return (undefined4 *)0x0; } ``` The resize/create path at `FUN_0044ccc0`: ```c undefined1 __thiscall FUN_0044ccc0(int *param_1, int param_2) { char cVar1; /* call vtable slot +0x04 — Allocate(width, bpp, ?, ?, ?) */ cVar1 = (**(code **)(*param_1 + 4))( *(undefined4 *)(param_2 + 8), /* source width */ *(byte *)(param_2 + 0xc), /* source bpp */ *(byte *)(param_2 + 0x14), *(byte *)(param_2 + 0x15), *(byte *)(param_2 + 0x18)); if (cVar1 == '\0') return 0; /* memcpy source pixels into param_1[4] */ /* mark dirty, return true */ } ``` So `PTR_FUN_0079c26c` is `CSurface`, vtable slots: | Slot | Method (guess) | |---|---| | `+0x00` | `~CSurface()` / scalar-deleting dtor | | `+0x04` | `bool Allocate(int w, byte bpp, byte, byte, byte)` | | `+0x08` | `void Release()` or `Destroy()` (no args) | And the struct layout: | Offset | Type | Field | |---|---|---| | `+0x00` | ptr | vtable (`PTR_FUN_0079c26c`) | | `+0x04` | int | width | | `+0x08` | int | height (or `dataSize`) | | `+0x0c` | byte | bytesPerPixel (default 2) | | `+0x10` | ptr | pixel buffer | | `+0x14` | byte | flag (palette?) | | `+0x15..17` | byte×3 | flags | | `+0x18` | byte | owns-buffer | | `+0x1c` | int | ? | | `+0x20` | int | ? | | `+0x24` | byte | flag | This is plumbing for Keystone's drawing surface — the client owns the pixels, Keystone owns the widget semantics. --- ## 6. The `CFont` class (`chunk_00440000.c`) A clean glyph-lookup implementation. Struct layout derived from `FUN_004434c0` (glyph lookup), `FUN_004435d0` (build range map), `FUN_00443580` (has-glyph check), and `FUN_00443960` (clear/reset): | Offset | Type | Field | |---|---|---| | `+0x00` | ptr | vtable | | `+0x30..0x44` | int×6 | padding / char-cell metrics | | `+0x38` | int | glyph count | | `+0x3c` | ptr | glyph array (11 bytes per entry) | | `+0x4c` | ptr | refcounted CString (font name) | | `+0x50..0x5c` | int×? | metrics (advance, line height, ascent, descent) | | `+0x60` | ptr | second refcounted CString (style?) | | `+0x64` | ushort | first character (codepoint) | | `+0x66` | ushort | last character | | `+0x68` | int | range span count | | `+0x6c` | ptr | ushort[] codepoint→glyph-index map | Glyph entry (11 bytes): | Offset | Type | Field | |---|---|---| | `+0x00` | ushort | unicode codepoint | | `+0x02..0x05` | byte×4 | bitmap rect / atlas coords | | `+0x06` | byte | advance A | | `+0x07` | byte | advance B | | `+0x08` | byte | advance C | | `+0x09` | byte | leading whitespace | | `+0x0a` | byte | trailing whitespace | `FUN_00443580` is the canonical `CFont::HasGlyph(ushort ch)`: ```c undefined1 __thiscall FUN_00443580(int self, ushort ch) { uint uVar1; if (*(int *)(self + 0x3c) != 0) { if (*(int *)(self + 0x6c) == 0) { return 1; /* no range map => font covers all glyphs */ } if (*(ushort *)(self + 0x64) <= ch && ch <= *(ushort *)(self + 0x66)) { uVar1 = (uint)*(ushort *)( *(int *)(self + 0x6c) + (ch - *(ushort *)(self + 0x64)) * 2); if (uVar1 < *(uint *)(self + 0x38)) { if (uVar1 * 0xb + *(int *)(self + 0x3c) != 0) return 1; } } } return 0; } ``` This is textbook font-range-map lookup. The retail client draws glyph bitmaps through this `CFont` and then Keystone composites them. acdream already has font rendering in the renderer, so we port this class' *struct layout* only if we want to consume the retail portal.dat font records directly (which is in scope for a future phase). --- ## 7. `CString` refcount helper — the text primitive Used everywhere. `chunk_00400000.c` has the family: * `FUN_00402490` — `CString::operator+=(const wchar_t *, size_t)` — append * `FUN_00407e40` — `CString::operator=(const wchar_t *)` — assign * `FUN_0040b8f0` — small wrapper that computes wcslen and appends (this is the function the preflight task was worried about) * `FUN_004022d0` — ensure capacity * `FUN_004027b0` — `sprintf`-style format into a CString * `FUN_004300a0` — `sprintf` append All of them manipulate a pimpl buffer reached through `*param_1`, with a header right before the string: | Offset (from `*param_1`) | Field | |---|---| | `-0x14` | vtable for refcount sub-object | | `-0x10` | `LONG` reference count (manipulated through `InterlockedIncrement`/`InterlockedDecrement`) | | `-0x0c` | capacity | | `-0x08` | length / state | | `-0x04` | length | | `+0x00` | wide-char buffer (null terminated) | Every wide-string literal the client uses — from `L"Drag necklaces here to wear them"` to `L"Select a spell to cast"` to `L"Attributes"` — is appended to one of these buffers, and the buffer is then pushed through the Keystone bridge (via vtable slots on `DAT_00870c2c`) to be rendered. acdream's C# port of this is trivial: we already have `System.String`, and the refcount machinery is a garbage-collector concern. The only thing we might need is a pooled `StringBuilder`-style reuse for tooltip hot paths, which is a future optimization, not something to mirror structurally. --- ## 8. What the input chunk (`chunk_00680000.c`) actually is The other candidate base I looked at — the class at `FUN_006895d0` that has `POINT` coords at offsets `+4,+8`, `PtInRect` calls, and flags at `+0x358` — turns out to be the **DirectInput/Win32 mouse and keyboard state manager**, not a widget. It: * Owns `HWND` at `+0x10c` * Tracks raw mouse position, last-click point, double-click timing * Calls `GetCursorPos` / `ScreenToClient` / `ClientToScreen` directly * Calls `FUN_00557a30` (= Keystone `GetActive()` via `DAT_00870c2c +0x2c`) and `FUN_00557a60` (= Keystone `HitTest(POINT*)`) to ask Keystone whether the mouse is currently over any Keystone widget before deciding whether to route the mouse to the 3D view So the input path is: 1. Windows sends `WM_MOUSEMOVE` (via Ghidra-undecompiled windowproc, not in the task-selected chunks). 2. The input manager (`chunk_00680000.c`) updates cached mouse state. 3. Each frame, the client calls `FUN_00557a30` / `FUN_00557a60` to check if Keystone owns the cursor. 4. If Keystone does, the 3D view ignores mouse input entirely; Keystone has already dispatched it internally. 5. If Keystone does not, the 3D click/drag goes to the world-selection path (physics raycasts into the scene). The "widget hit test" lives inside Keystone, not inside `acclient.exe`. There is no local `IsPointInside(point)` virtual method on the client side. --- ## 9. The main UI frame dispatch (`FUN_0043fcd0` in `chunk_00430000.c`) The only place the UI per-frame pump is visible from the decompile: ```c void FUN_0043fcd0(void) { if ((char)DAT_00870340[0x2b] != '\0') { int iVar1 = DAT_00870340[0x24]; /* back buffer width */ int iVar2 = DAT_00870340[0x23]; /* back buffer height */ int iVar3 = *DAT_00870340; /* graphics-device vtable */ uVar4 = FUN_0054fd30(0); /* get clear color */ uVar4 = FUN_0054fd20(uVar4); (**(code **)(iVar3 + 0x40))(0, 0, uVar4); /* Device::Clear() */ if (unaff_BL != '\0' && DAT_00818c0c != '\0') { FUN_004488a0(); /* draw 3D scene */ FUN_00557840(); /* Keystone->ProcessFrame() = draw + tick UI */ } if (DAT_0083846c != 0) FUN_005da8f0(); /* overlay #1 */ if (DAT_00838468 != 0) FUN_00692470(); /* overlay #2 */ FUN_0043f7f0(); /* final 2D overlays (cursor, tooltip) */ (**(code **)(*DAT_00870340 + 0x40))( iVar2, iVar1, unaff_EDI, uVar6, 0); /* Device::Present() */ (**(code **)(*DAT_00870340 + 0x24))(); /* post-present housekeeping */ (**(code **)(*DAT_00870340 + 0x28))(); FUN_0043e6b0(); } } ``` This is unambiguous: 1. Clear. 2. Render 3D world. 3. Hand the frame to Keystone for UI draw. 4. Render extra native overlays. 5. Present. The UI layer is a *single* subsystem the client calls into between 3D and present. Everything inside Keystone — panels, widgets, text entry, scrolling, layout — is opaque to `acclient.exe`. --- ## 10. Explicit answers to the task's checklist questions 1. **Base UI element class and its vtable.** There isn't one baked into `acclient.exe`. The external `IKeystoneFramework` interface at `*DAT_00870c2c` is the closest analog, with observed methods `Release` (`+0x08`), `FindPlugin(name)` (`+0x14`), `ProcessFrame` (`+0x20`), `CreateOrActivate(a,b,c,d)` (`+0x24`), `ClearActive` (`+0x28`), `GetActive` (`+0x2c`), `SendCommand(id, op, buf)` (`+0x5c`), `HitTest(POINT*)` (`+0x60`), `TranslateAccelerator` (`+0x6c`). Keystone's *own* widget base class is inside `keystone.dll`, which is not part of this decompile. 2. **Hierarchy by examining subclass extensions.** The only class hierarchy fully resident in the client is UI-manager + listener: `CUIManager` (vtable `PTR_FUN_00799fc4`, singleton `DAT_00838374`) and the listener base `CUIListener` (vtable `PTR_FUN_00801670`). Panels that want to be notified embed the listener by multiple inheritance; I can count ~25–30 such embeddings in the chunks I surveyed (CharGen at 0x0047aa10 is the canonical example; others are distributed through `chunk_00470000.c`, `chunk_004A0000.c`, `chunk_004C0000.c`, `chunk_004E0000.c`, and ~20 more). None of these are visual widgets; they are game-state observers that use the UI-manager singleton as a notification hub. 3. **Common struct layout / consistent offsets.** The client classes do not share a common "x/y/w/h at consistent offset" pattern. The one layout convention that *is* consistent is MI: the secondary vtable of the listener base lives at a fixed offset inside the derived object (e.g. `+0x12c` for CharGen, `+0x5f8` for the CharGen screen, varying by class size), and that inner `this` is what gets passed to `CUIManager::AddListener`. There is no common geometry field. 4. **Polymorphism pattern.** Classic Microsoft C++ **multiple inheritance with per-base-class vtables at known offsets**. Primary vtable at `+0x00`; secondary / tertiary bases at `+0x04`, `+0x08`, or at deeper offsets like `+0x12c` and `+0x5f8` as their subobjects are laid out. Method dispatch is `(**(code **)(*obj + slot))(obj, …)` against whichever vtable the caller holds a pointer to. This is not a tagged union or a discriminated enum — every polymorphic operation in the client is a vtable call. 5. **Container / panel class with a child list.** None in `acclient.exe`. Keystone owns the containment model. 6. **Button / clickable class.** None in `acclient.exe`. Buttons are Keystone-side; the client only sends a command-id via `SendCommand` or a plugin-dispatch call. 7. **Text / label class.** Not a class. Labels are `wchar_t *` literals assigned to `CString` buffers through `FUN_00407e40` / `FUN_0040b8f0`. The buffers are then passed as resource contents to Keystone — the client has no `CLabel`. 8. **Edit-box / text-entry class.** The client has the `CString` back end (so paste-into-chat, type-in-username, etc. work), and the client calls `ImmGetContext` / `ImmAssociateContext` (`FUN_00557850`) to let the IME talk to Keystone's focused edit control, but the edit control itself is inside Keystone. 9. **List / scrolling class.** Not in `acclient.exe`. 10. **Pseudocode for 4–5 base-class virtual methods.** Since the widget base does not exist in this binary, the only thing I can write pseudocode for is the Keystone-bridge layer and the listener/UI-manager pair. See section 11. 11. **Equivalent C# base class + hierarchy.** See section 11. --- ## 11. Recommended C# hierarchy for acdream Given that retail delegated to Keystone and Keystone is not in the decompile, we have two paths: **(A) Implement our own toolkit on top of our existing renderer.** This is the honest pragmatic choice and matches what AC2D did. The C# base class acdream should adopt is a retained-mode scene graph with per-node rectangle, children list, parent pointer, z-order, visibility, and virtual hit-test / draw / tick / key / mouse. This is strictly acdream's choice, not a port — retail's choice was "delegate to Keystone", and Keystone is the part we cannot port. ```csharp // AcDream.App/UI/UiElement.cs public abstract class UiElement { // --- Geometry --- public float Left { get; set; } public float Top { get; set; } public float Width { get; set; } public float Height { get; set; } // Absolute screen-space rect (computed by walking Parent chain). public Rectangle AbsoluteBounds => /* Parent-offset aware */; // --- Hierarchy --- public UiElement? Parent { get; internal set; } public IReadOnlyList Children => _children; private readonly List _children = new(); // --- State --- public bool Visible { get; set; } = true; public bool Enabled { get; set; } = true; public int ZOrder { get; set; } // higher = in front public bool CapturesMouse { get; set; } // for drag operations // --- Events (flat, not per-type-abstractor like AC2D) --- public event Action? MouseDown; public event Action? MouseUp; public event Action? MouseMove; public event Action? MouseWheel; public event Action? KeyDown; public event Action? KeyUp; public event Action? TextInput; public event Action? GotFocus; public event Action? LostFocus; // --- Virtuals that subclasses override --- // Concrete widgets (UiPanel, UiLabel, UiButton, UiEditBox, UiScrollBar) // override OnDraw; everything else has a sensible default. protected abstract void OnDraw(UiRenderContext ctx, double alpha); protected virtual void OnTick(double deltaSeconds) { } protected virtual bool OnHitTest(float x, float y) => x >= 0 && x < Width && y >= 0 && y < Height; protected virtual bool OnMouseMessage(MouseMessage m) => false; protected virtual bool OnKeyMessage(KeyMessage m) => false; protected virtual bool OnTextInputMessage(TextInputMessage m) => false; // --- Parent API --- public void AddChild(UiElement child) { if (child.Parent != null) child.Parent.RemoveChild(child); child.Parent = this; _children.Add(child); } public bool RemoveChild(UiElement child) { if (!_children.Remove(child)) return false; child.Parent = null; return true; } // --- Framework entry points (called by UiRoot) --- internal void Draw(UiRenderContext ctx, double alpha) { if (!Visible) return; OnDraw(ctx, alpha); // Children sorted by ZOrder ascending (painter's algorithm). foreach (var c in _children.OrderBy(c => c.ZOrder)) c.Draw(ctx, alpha); } internal void Tick(double dt) { OnTick(dt); for (int i = 0; i < _children.Count; i++) _children[i].Tick(dt); } internal UiElement? HitTest(float localX, float localY) { if (!Visible || !Enabled) return null; // Children first (top of Z), then self. for (int i = _children.Count - 1; i >= 0; i--) { var c = _children[i]; var hit = c.HitTest(localX - c.Left, localY - c.Top); if (hit != null) return hit; } return OnHitTest(localX, localY) ? this : null; } } // Concrete subclasses: public class UiPanel : UiElement { /* draws a texture 9-slice background */ } public class UiLabel : UiElement { public string Text; public Color Color; } public class UiButton : UiPanel { public string Text; public event Action? Click; } public class UiEditBox : UiElement { public string Text; public bool MultiLine; } public class UiScrollBar : UiElement { public int Min, Max, Value; public bool Horizontal; } public class UiImage : UiElement { public uint DatPictureId; } public class UiList : UiElement { /* virtualized scroll list */ } public class UiWindow : UiPanel { public string Title; public bool CanClose; public bool CanMove; public bool CanResize; } // Top of tree: public class UiRoot : UiElement { // Owns focus, drag capture, cursor state. Called once per frame from // AcDream.App.Rendering.GameWindow after the 3D scene and before Present. public void DispatchFrame(double dt, InputSnapshot input, UiRenderContext ctx) { Tick(dt); HandleInput(input); Draw(ctx, alpha: 0.0); } } ``` Pseudocode for the four critical base virtuals follows retail's *spirit* (depth-first child traversal, children-first hit testing, Z-order painter's algorithm, short-circuit event delivery) without trying to match bit-for-bit a layout that isn't actually in the retail client. ```pseudo function UiElement.Draw(ctx, alpha): if not visible: return OnDraw(ctx, alpha) # own content for c in children sorted by ZOrder: ctx.PushTranslate(c.Left, c.Top) c.Draw(ctx, alpha) ctx.PopTranslate() function UiElement.Tick(dt): OnTick(dt) # animations, caret blink, etc. for c in children: c.Tick(dt) function UiElement.HitTest(x, y): if not visible or not enabled: return null # Children are painted back-to-front, so hit-test front-to-back. for c in reversed(children ordered by ZOrder): hit = c.HitTest(x - c.Left, y - c.Top) if hit != null: return hit return self if OnHitTest(x, y) else null function UiElement.HandleMouse(msg): # msg.X/msg.Y are in parent coords; convert to local on each recursion. if msg.Type == MouseMove and capturing_element != null: return capturing_element.HandleMouse(msg.TranslatedTo(capturing_element)) hit = HitTest(msg.X - Left, msg.Y - Top) if hit == null: return false # Walk from hit up through ancestors until someone handles the event. walker = hit while walker != null: if walker.OnMouseMessage(msg.TranslatedTo(walker)): return true walker = walker.Parent return false function UiElement.HandleKey(msg): # Keyboard goes to the focused element with ancestor bubble-up. target = focused_element ?? self while target != null: if target.OnKeyMessage(msg): return true target = target.Parent return false ``` **(B) Try to reverse-engineer and reimplement Keystone.** This would require decompiling `keystone.dll` (not in our decompile set) and understanding the resource-ID dispatch model. This is a real option long term if we want genuine retail look-and-feel, but it is out of scope for the present task. For now, acdream should take path (A) and implement a lean toolkit that renders the SAME dat-borne textures (icons, 9-slice frames) and the SAME CFont glyphs that the retail client used; that preserves the visual identity even though the class hierarchy underneath is new. --- ## 12. Implications for acdream * There is no "decompile first, port faithfully" path for the widget tree, because the widget tree lives in `keystone.dll`. For the UI, the decompile tells us: 1. Which *primitives* the retail client provides to Keystone (`CFont`, `CSurface`, `CString`) and their struct layouts — we can port these if and when we need to consume retail portal.dat font records or embed-atlas records. 2. Which *commands* and *plugins* the retail client exchanges with Keystone (resource IDs like `0x186a1`, plugin names like `L"acpluginmanager"`, command codes like `0x69`) — we can cache these if we ever want to pretend to be Keystone for a plugin. 3. The **frame pump shape** (clear → scene → UI → overlays → present) — acdream already follows this; no change needed. * The follow-on research slices (layout data, input pipeline, etc.) should assume path (A). If any slice finds evidence of an in-binary widget they should flag it, but they won't — I've walked the listener tree and it isn't there. * The `docs/architecture/acdream-architecture.md` UI-layer section should be updated to state: "acdream implements its own retained-mode UI in `AcDream.App/UI/`; the retail binary delegates to Keystone, which acdream does not reimplement; our UI is visually consistent with retail by consuming the same portal.dat textures and fonts, not by structural class equivalence." --- ## 13. Loose ends the next agent should know about * `DAT_00870340` is **not** a UI object. It is the Direct3D swap-chain / back-buffer wrapper (width at `+0x94`, height at `+0x90`, Clear / Present vtable at `+0x40` / `+0x44`, `Reset` at `+0x18`, `0x24`, `0x28`). It is called everywhere because every 2D overlay and every 3D draw needs it. I originally thought the `*(char *)(DAT_00870340 + 0x10) == '\0'` check was a UI visibility flag; on re-reading, it is the "graphics device is active / not minimized" flag that controls cursor behaviour (`LoadCursorA(0x7f00)` fallback at `chunk_00430000.c:7862`). The `DAT_00870340[0x2b]` byte in `FUN_0043fcd0` is the "render is allowed this frame" gate. * `DAT_00837ff4` is the **input** subsystem pointer, with a vtable that has `IsCursorVisible(bool)` at `+0x74` (via the ShowCursor thunk at `chunk_00430000.c:7840`). Not UI either. * `DAT_0083e72c` is a singleton pointer for the CharGen top-level class; `FUN_004799c0` sets it. There is exactly one CharGen screen at a time. That confirms the CharGen-screen-is-a-listener model. * Vtable `PTR_FUN_007ccb60` is used as a temporary "placeholder" vtable during construction and destruction. Every object that has a listener subobject installs `PTR_FUN_007ccb60` first, does some plain-base work, then swaps in the real listener vtable, and reverses this at teardown. Useful to recognize when reading constructors. * The `0x186a1`, `100000` (`0x186a0`), `0x186a4` etc. IDs in the CharGen ctor/dtor look like Keystone resource IDs (they are contiguous and decimal-friendly: 100000, 100001, 100002, 100003, 100004). If any future task needs to drive Keystone directly, these are the channels. * `0x10000001` … `0x1000003e` etc. are similar IDs passed to `FUN_004e8190`, `FUN_004e90d0`, etc. — those are `CharGen` state- machine event IDs (the CharGen is a state machine bridged into the UI through the same manager singleton). Not widgets. * `FUN_0054d2a0` returns a `CSurface` ask-or-allocate result; the calling code assumes the singleton graphics device will hand back a pooled surface when the device exists, else the client allocates its own 40-byte surface. This is the code path to match when we implement acdream's UI texture cache. --- ## 14. Minimum additional chunks a follow-up UI pass should read If a later research task revisits this area, these chunks deserve a closer pass than I gave them: * `chunk_00430000.c` around `FUN_0043c6c0` → `FUN_0043cf00` — full shape of the UI-manager / listener tables. * `chunk_00550000.c` around `FUN_00557930` → `FUN_00557ef0` — Keystone glue layer, complete vtable documentation of `DAT_00870c2c`. * `chunk_00470000.c` around `FUN_0047aa10` → `FUN_0047b2e0` — CharGen screen state machine in full, and its registration IDs. * `chunk_004A0000.c` around `FUN_004a5200` → `FUN_004a5680` — the paperdoll slot tooltip formatter; shows the complete set of "drag X here to wear it" patterns and therefore the slot-to-string table. * `chunk_004C0000.c` around `FUN_004c7700` → `FUN_004c7f00` — spell casting UI integration. None of these will change the conclusion in this document — they will flesh out the *commands and strings* the client exchanges with Keystone, not produce a hidden widget base class.