# 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<UiElement> Children => _children;
    private readonly List<UiElement> _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<UiElement, MouseButtonEvent>? MouseDown;
    public event Action<UiElement, MouseButtonEvent>? MouseUp;
    public event Action<UiElement, MouseMoveEvent>?   MouseMove;
    public event Action<UiElement, MouseWheelEvent>?  MouseWheel;
    public event Action<UiElement, KeyEvent>?         KeyDown;
    public event Action<UiElement, KeyEvent>?         KeyUp;
    public event Action<UiElement, TextInputEvent>?   TextInput;
    public event Action<UiElement>?                   GotFocus;
    public event Action<UiElement>?                   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.
