# Retail AC Client — UI Architecture, Initialization, and Main Loop

**Research slice:** process entry → window → device → UI → frame loop.
**Method:** decompiled `acclient.exe` (22,225 functions via Ghidra pyghidra
headless); cross-check against vendored reference repos where they cover
the same topic.

Unless noted otherwise, every function and global cited here is taken
directly from the decompiled source in
`C:\Users\erikn\source\repos\acdream\docs\research\decompiled\chunk_*.c`.
The reference repos under `references/` (ACE, ACViewer, WorldBuilder,
holtburger, Chorizite.ACProtocol, AC2D) do **not** cover the in-process
UI layer at all — they are emulators, dat viewers, or protocol libraries.
Everything below about Keystone / the main window / the frame loop is
novel to this slice.

---

## 0. TL;DR for the porting agent

The retail client is a single-window, single-process, single-render-thread
C++ app built around three "Cxxx" globals:

| Global (`DAT_`) | Role | Acquired by |
|-----------------|------|-------------|
| `DAT_008381a4` (HWND) | Main game window (`"Turbine Device Class"`) | `CreateWindowExA` @ 0x0043BD0B |
| `DAT_0086734c` (ptr) | **Graphics driver adapter** (COM-like factory) | `FUN_0054d0c0` → `FUN_0058bf30` |
| `DAT_00870340` (ptr) | **Turbine Core** — main engine object that owns the GL surface, dat resources, and UI tools | `FUN_0054d110` → `DAT_0086734c` vtable[0xc] |
| `DAT_00837ff4` (ptr) | **Render device** (window-specific, lightweight) | `FUN_006895d0` @ 0x006895D0 |
| `DAT_00870c30` (HMODULE) | `keystone.dll` | `FUN_00557930` |
| `DAT_00870c34` (code*) | `KeystoneCreate` export | `GetProcAddress` |
| `DAT_00870c2c` (ptr) | **Keystone UI root** (instance returned by `KeystoneCreate`) | `FUN_00557850` |
| `DAT_00870c38/3c` (HMODULE) | `ACHelpPlugin.dll`, `ACPluginManager.dll` | `LoadLibraryA` |
| `DAT_00870c54` (HACCEL) | Accelerator table | `CreateAcceleratorTableA` |
| `DAT_00818b64` | Packed (width << 16 | height) of backbuffer | various WM_SIZE paths |
| `DAT_00838194` | Quit flag ("app should exit") | `FUN_00439230` (`QuitApp`) |
| `DAT_00838196` | "Main window alive" flag | `FUN_0043BA60` tail |
| `DAT_00838198` | "Currently pumping messages" re-entrancy guard | `FUN_00439e50` |
| `DAT_00838199` | "Redraw requested" flag | WM_PAINT path |
| `DAT_008381a8` | 1 when app was launched with cmdline args | `FUN_0043BA60` |

The UI framework is **Keystone** (proprietary Turbine XML-based UI toolkit
— same engine later used in Lord of the Rings Online / DDO). It is shipped
as `keystone.dll` next to `acclient.exe` and exposes a single C export,
`KeystoneCreate`, that returns a vtable-dispatched root object. All
panels, buttons, fonts, textures, and input are hidden behind that one
pointer. AC is a Keystone *client*; it does not own the UI code.

The frame loop runs three things back-to-back:

1. **Message pump** (`FUN_00439e50`) — `PeekMessageA` + `TranslateMessage`
   + `DispatchMessageA` until the queue is empty. The app's WndProc
   (`LAB_00439860`) hands each message first to the UI filter
   (`FUN_00557a90` → Keystone's vtable[0x6c]), then to the renderer
   (`DAT_00837ff4` vtable[0x70]). `DefWindowProcA` is the fallthrough.
2. **Game tick** (`FUN_004554b0`) — advances game state / physics /
   scripts; also drains per-frame command queues.
3. **Render frame** (`FUN_0045d0b0`) — BeginScene-equivalent, draws
   3D world, draws UI as overlay, EndScene-equivalent, present.

---

## 1. Process entry and window-class registration

### 1.1 Entry point (WinMain equivalent)

```
CRT startup @ 0x005DF1xx (chunk_005D0000.c:~11280)
   └─ __getmainargs, _initterm, parse cmdline
   └─ GetStartupInfoA, GetModuleHandleA(NULL)
   └─ FUN_004013a0(hInstance, NULL, lpCmdLine, nShowCmd)  // <-- WinMain
          │
          ├─ FUN_00406300()        // preflight checks
          ├─ FUN_00406d60()        // exception handler setup
          ├─ GetCommandLineA()
          ├─ FUN_00401120()        // _control87 — disable FPU exceptions
          ├─ DAT_00837720 = 0x40000001
          ├─ FUN_00413850(0, &PTR_DAT_008183b4)  // mount factory table
          ├─ FUN_0055af00 / 00555990 / 00558230  // setup log / dat / COM
          ├─ piVar2 = FUN_00401160(&DAT_007936b8) // factory: App object
          ├─ piVar2->vtable[0x10](0, 0, 1)       // Application::Initialize
          ├─ FUN_00401340("Asheron's Call")       // title string
          ├─ piVar2->vtable[0x1c](stack, 1, 1)   // Application::Run    <-- main loop lives here
          ├─ piVar2->vtable[0x2c]()              // Application::Shutdown
          ├─ FUN_004010f0(piVar2)                // release singletons
          ├─ FUN_004020c0 / FUN_00406f90          // teardown
          └─ return 0
```

- **WinMain = `FUN_004013a0` at 0x004013A0** (decompiled as
  `chunk_00400000.c:288-341`).
- The App object is a virtual class; its vtable is at
  `&DAT_007936b8` (chunk_00400000.c:313). Slots we care about:
  - `[0x10]` = `Initialize(hInstance, pfnMsg, isRestart)` — see
    FUN_00412180 @ 0x00412180 (chunk_00410000.c:1785+)
  - `[0x1c]` = `Run(p1, p2, p3)` — calls into one of the two frame
    pump variants below
  - `[0x2c]` = `Shutdown()` — unclear which FUN_ slot

### 1.2 Application::Initialize (vtable[0x10])

Decompiled as `FUN_00412180`:

```c
bool Application::Initialize(pfnMsg, lpCmdLine, nShowCmd, ...) {
    _set_new_handler(LAB_00411580);
    if (this->vtable[0x80](this))   // pre-init hook; bails out if app
        return false;               // is a secondary instance

    GetVersionExA(...);
    if (version == Win9x)
        LoadLibraryA("unicows.dll");  // Unicode shim on Win98/ME

    FUN_0040fcd0();   // init dat file cache
    FUN_0042c800();   // init string tables
    this->field_0x41 = FUN_0054bb50();  // singleton: audio
    this->field_0x42 = FUN_0054bb70();  // singleton: renderer-factory wrapper

    this->vtable[0x68]();                                 // network bind setup
    if (!this->vtable[0x70]())                           // preferences
        return false;
    if (!this->vtable[0x74](lpCmdLine, nShowCmd, hasArgs)) // window + D3D init (see §2)
        return false;
    if (!FUN_004221c0())                                  // acqr locator
        return false;
    return this->vtable[0x7c](pfnMsg) != 0;              // final wiring
}
```

Key takeaway: vtable[0x74] is the "create the window AND initialize the
render device" method, and its call site is what drives all of §2.

### 1.3 Window-class registration and CreateWindow

All from `chunk_00430000.c:9848-10046`. This is a single ~1100-byte
function that does window-class registration, window creation, renderer
device creation, UI wiring, and chat-command registration in one go.
Call it **`CreateMainWindow`** (no FUN_ label visible because it's
reachable only via vtable[0x74]).

```c
CreateMainWindow(titleStr, x, y, w, h, fullscreen, iconId) {
    if (DAT_00838196 != 0) return 0;        // already created

    GetVersionExA(&local_94);
    DAT_0083819c = local_94.dwPlatformId;   // 0 = Win9x, 2 = NT
    if (local_94.dwPlatformId == 0)         // explicit "no Win9x" check
        goto fail;

    hInstance = GetModuleHandleA(param[0]); // resolves exe module
    if (DAT_0086734c != 0) return 0;        // renderer already alive
    FUN_0054d0c0(&local_f0);                // build graphics-driver factory
                                             //   → DAT_0086734c
    FUN_0043b2d0();                          // parse --window / --fullscreen
    FUN_00439140();                          // defaults: 800×600×32bpp
    FUN_00439370(&local_d8);                 // sanitize size (≥ 800 × ≥ 600)

    //--- step 1: register class "Turbine Device Class" ---
    local_bc.lpfnWndProc   = (WNDPROC)&LAB_00439860;     // <-- WndProc
    local_bc.cbClsExtra    = 0;
    local_bc.cbWndExtra    = 0;
    local_bc.style         = 0;
    local_bc.hInstance     = hInstance;
    local_bc.hIcon         = LoadIconA(hInstance, (LPCSTR)0x65);
    local_bc.hbrBackground = GetStockObject(BLACK_BRUSH); // 4
    local_bc.lpszMenuName  = (LPCSTR)*titleStr;
    local_bc.hCursor       = NULL;                       // cursor set later
    local_bc.lpszClassName = "Turbine Device Class";
    AVar3 = RegisterClassA(&local_bc);
    if (!AVar3 && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) goto fail;

    //--- step 2: choose style; center on desktop; apply workarea ---
    DWORD style = fullscreen ? 0x82000000
                             : 0x82ca0000 | (isPopup ? 0x10000000 : 0);
    // adjust for SM_CXFRAME / SM_CYFRAME; SystemParametersInfoA(SPI_GETWORKAREA)
    // snaps window inside the work area (accounts for taskbar).

    //--- step 3: CreateWindowEx ---
    DAT_008381a4 = CreateWindowExA(
        0, "Turbine Device Class", titleStr, style,
        x, y, w, h, NULL, NULL, hInstance, NULL);
    if (!DAT_008381a4) goto fail;

    //--- step 4: allocate render device (FUN_006895d0) ---
    if (FUN_005df0f5(0x3f8))
        DAT_00837ff4 = FUN_006895d0();       // see §3.1
    cVar2 = DAT_00837ff4->vtable[4](DAT_008381a4);    // device->AttachWindow
    if (!cVar2) return 0;

    if (showWindow) {
        ShowWindow(DAT_008381a4, SW_SHOWNORMAL);
        UpdateWindow(DAT_008381a4);
        SetForegroundWindow(DAT_008381a4);
        SetActiveWindow(DAT_008381a4);
        SetWindowPos(DAT_008381a4, HWND_TOPMOST, 0,0,0,0,
                     SWP_NOMOVE|SWP_NOSIZE|SWP_NOACTIVATE|0x100);
    }

    //--- step 5: bring up renderer + UI (FUN_0043ad90, see §3) ---
    if (!FUN_0043ad90(param_d8, param_d4, isFullscreen))
        return 0;

    //--- step 6: audio & console ---
    if (FUN_005df0f5(8))
        DAT_008381ac = FUN_00439210();       // ImmDisableIME equivalent

    //--- step 7: register ~30 chat / console commands ---
    FUN_00401340("Exits the application");
    FUN_00401340(&DAT_00799d90);    // name: "Quit" or "Exit"
    FUN_00436580(&LAB_00439830, ...);
    // ...many more...
    FUN_00401340("Restarts the rendering engine and applies new display settings");
    FUN_00401340("UpdatePresentation");
    FUN_00436580(FUN_0043a510, ...);
    FUN_00401340("ForceDisplayResolution [<Width> <Height>]");
    FUN_004366d0(FUN_0043aa70, ...);

    SetThreadExecutionState(0x80000001);  // keep display on / prevent sleep
    DAT_00838196 = 1;                       // "main window alive"
    return 1;
}
```

### 1.4 The window procedure itself (`LAB_00439860`)

Ghidra did not emit a `FUN_00439860` block — the gap between
`FUN_00439840` (ends ~0x0043985D) and `FUN_00439d50` holds it but the
decompiler skipped producing C for it, likely due to inline asm or a
non-standard prolog. We can still reconstruct it from the message pump
(§4), which tells us every consumer it forwards to:

```
LRESULT CALLBACK TurbineWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
    // 1. Give Keystone first shot (panels eat mouse/keyboard first).
    if (DAT_00870c2c != NULL) {
        LRESULT ui = DAT_00870c2c->vtable[0x6c](hwnd, msg, wp, lp);
        if (ui != 0)
            return ui;   // UI swallowed it
    }

    // 2. Give the render device a chance (WM_ACTIVATE, WM_SIZE,
    //    WM_DISPLAYCHANGE, WM_PAINT, WM_ERASEBKGND).
    if (DAT_00837ff4 != NULL) {
        LRESULT rd = DAT_00837ff4->vtable[0x70](&msg_struct, &handled);
        if (handled) return rd;
    }

    // 3. Minimal direct handling in the WndProc itself:
    //    WM_CLOSE → DAT_00838194 = 1 (quit flag)
    //    WM_ACTIVATEAPP → DAT_00818b68 toggles
    //    WM_SETCURSOR → LoadCursorA(NULL, IDC_ARROW)
    //    WM_CHAR / WM_KEYDOWN → forwarded to chat command parser

    // 4. Default
    return DefWindowProcA(hwnd, msg, wp, lp);
}
```

The fact that the pump calls `TranslateMessage` only if both
`FUN_006a1050` (IME filter, stub returning 0) and `FUN_00557a90`
(Keystone peek-in) return 0 is the smoking gun. See §4.

### 1.5 Global state planted by window creation

| Address | Type | Meaning |
|---------|------|---------|
| `DAT_008381a4` | HWND | The single client HWND |
| `DAT_0083819c` | int | Win platform ID (2=NT family) |
| `DAT_00838194` | bool | Quit requested |
| `DAT_00838195` | bool | Renderer alive |
| `DAT_00838196` | bool | Window alive |
| `DAT_00838197` | bool | Cursor hidden |
| `DAT_00838198` | bool | Inside PeekMessage loop (guard) |
| `DAT_00838199` | bool | Redraw requested |
| `DAT_008381a0` | bool | Fullscreen mode |
| `DAT_008381a8` | int | 1 if started with cmdline args |
| `DAT_00818b02` | bool | Show-frame (windowed mode) flag |
| `DAT_00818b04/08` | int | Windowed size (for restore) |
| `DAT_00818b64` | packed | (width<<16 | height) of render target |
| `DAT_00818b68..72` | bytes | Windowed-vs-fullscreen state mask |

---

## 2. Renderer / device bring-up

Two objects; order matters.

### 2.1 Graphics-driver factory (`DAT_0086734c`)

```
FUN_0054d0c0(&mode_out)
    DAT_0086734c = FUN_0058bf30()         // allocate the factory COM-ish
    DAT_0086734c->vtable[4](&mode_out)    // Init — probes available backends
```

`FUN_0058bf30` is in `chunk_00580000.c`; it's a factory that internally
picks the best of (DirectX 6 DirectDraw / Glide / software). None of our
modern clients will have to emulate that stack — we pick Silk.NET OpenGL
unconditionally. But we DO have to replicate the TWO-object factoring
(factory vs core) because so much of the code calls through
`DAT_0086734c->vtable[0xc]` to get the core.

### 2.2 Turbine Core (`DAT_00870340`)

`FUN_0054d110(hwnd, backbufW, backbufH)`:

```c
DAT_00870340 = DAT_0086734c->vtable[0xc]();   // CreateCore factory method
if (!DAT_00870340) return 0;
return DAT_00870340->vtable[4](hwnd, w, h);   // Core::Init(hwnd, w, h)
```

Turbine Core owns:
- GL / DirectDraw surfaces
- dat file handles (portal.dat, cell.dat, client_portal.dat, etc.)
- font atlases
- keystone instance handle (at `+0x468` — see §5.2)
- field at `+0x10` = "isFullscreen"
- vtable[0x40] = Present / Flush

### 2.3 Render device (`DAT_00837ff4`)

`FUN_006895d0()` constructs a lightweight device record (234 bytes of
zeroed fields plus two vtable pointers). It's allocated **before**
`Turbine Core` because the WndProc needs something pointable even during
the WM_CREATE that fires from `CreateWindowExA`.

It calls `CoInitialize(NULL)` at construction time. Field layout (offsets
in dwords):

| Offset | Meaning |
|--------|---------|
| +0    | vtable = `PTR_FUN_008000c8` |
| +0x40..0x46 | misc zero-init fields |
| +0x47..0x49 | 0xFFFFFFFF sentinels |
| +0x4a | vtable = `PTR_FUN_00800088` — sub-object |
| +0x67 | vtable = `PTR_FUN_0080008c` — sub-object |
| +0xd6 | hash-bucket count |
| +0xf3..0xf5 | state bits |
| +0xf8 | 1 (enabled flag) |

Vtable slots observed in the wild (§1.4, §3, §7):

| Slot | Role |
|------|------|
| 0x04 | AttachWindow(hwnd) |
| 0x10 | BeginFrame / BeginScene |
| 0x18 | GetWidth |
| 0x1c | GetHeight |
| 0x34 | SetTransform / SetRenderState |
| 0x38 | SetLight |
| 0x3c | DrawPrimitive (or DrawIndexed) |
| 0x48 | Present / EndFrame |
| 0x4c | DrawScreenQuad |
| 0x58 | IsDeviceLost |
| 0x70 | WndProc filter — returns LRESULT + handled-bool |
| 0x74 | SetCursorConfinement(bool) |
| 0xa8 | SetAlphaTest(bool) |

### 2.4 Post-device UI handshake (`FUN_0043ad90` → `FUN_0043ac60` → `FUN_0054e1a0`)

```c
FUN_0043ad90(w, h, isFullscreen) {
    DAT_00838195 = 0;
    if (!FUN_0043ac60(w, h, isFullscreen)) return 0;
    DAT_00838195 = 1;
    return 1;
}

FUN_0043ac60(w, h, isFullscreen) {
    if (isFullscreen) {
        hdc = CreateICA("Display", NULL, NULL, NULL);
        int bpp = GetDeviceCaps(hdc, BITSPIXEL);
        DeleteDC(hdc);
        if (bpp == 16) FUN_0043a8f0();           // 16bpp mode
        else if (bpp != 32) { print_error(); exit(1); }
    }
    if (!FUN_00439370(&size))  return 0;
    size.hwnd1 = DAT_008381a4;
    size.hwnd2 = DAT_008381a4;
    if (!FUN_0054e1a0(&size1, &size2)) return 0;   // core init
    if (!FUN_004402d0())                 return 0; // resource init
    FUN_0054e6a0();                                 // late wiring
    return 1;
}

FUN_0054e1a0(p1, p2) {
    if (!FUN_004154a0()) return 0;                 // probe — always true
    if (FUN_0044b810()) {                          // audio mixer ready?
        if (FUN_0054d110(DAT_0086734c[2], p1, p2)) {  // Turbine Core init
            FUN_00557850();                           // -> Keystone create (§5)
            if (FUN_00448810()) return 1;              // app-specific hook
            FUN_00557b50();                           // Keystone destroy on fail
            FUN_0044b820();                           // audio teardown
            FUN_0054d160();                           // core teardown
        }
    }
    // unwind DAT_0086734c
    return 0;
}
```

So the canonical first-run order is:

```
CreateWindowEx
  → DAT_00837ff4 = CRenderDevice()    // light shim
  → DAT_00837ff4->AttachWindow(hwnd)
  → (ShowWindow / UpdateWindow)
  → FUN_0043ad90 → FUN_0043ac60:
       → DAT_00870340 = CreateCore()  // Turbine Core
       → Core->Init(hwnd, w, h)       // real GL surface, dats online
       → KeystoneCreate(hwnd, ...)    // UI system (§5)
       → Application::PostDeviceInit  // game wiring (FUN_00448810)
```

---

## 3. Render device — frame-side vtable usage

The device is the 4th global (`DAT_00837ff4`). The frame loop calls its
vtable[0x10] to start a frame (`BeginFrame`) and vtable[0x48] to present
(`EndFrame`). BeginFrame happens inside `FUN_0045d0b0` (see §6.3); the
final `Present` is inside the Turbine-Core flush (`FUN_0043fcd0`) via
`DAT_00870340->vtable[0x40](w, h, ...)`.

Present path (chunk_00430000.c:12978):

```c
void RenderFrameFlushAndPresent() {
    if (!DAT_00870340->fieldIsFullscreen) return;  // offset +0x2b
    // grab backbuffer dims
    int w = DAT_00870340->field_0x24;              // width
    int h = DAT_00870340->field_0x23;              // height
    void* core = DAT_00870340->vtable0;            // core vtable
    uint t1 = FUN_0054fd30(0);                     // timing tick
    uint t2 = FUN_0054fd20(t1);
    DAT_00870340->vtable[0x40](0, 0, t2);          // Begin2DPhase?
    if (condition) {
        FUN_004488a0();                             // flush ui event queue
        FUN_00557840();                             // Keystone frame end
    }
    if (DAT_0083846c) FUN_005da8f0();              // cinematic overlay
    if (DAT_00838468) FUN_00692470();              // video subsystem
    FUN_0043f7f0();                                 // perf overlay
    DAT_00870340->vtable[0x40](h, w, ...);         // End2DPhase
    DAT_00870340->field_0x2a = 0;
    DAT_00870340->vtable[0x24]();                  // Present backbuffer
    DAT_00870340->vtable[0x28]();                  // Swap / flip
    FUN_0043e6b0();                                 // reset per-frame counters
}
```

---

## 4. Main-loop message pump (`FUN_00439e50`)

Address: `0x00439E50` in `chunk_00430000.c:8265-8297`, size 213 bytes.
Returns `DAT_00838194` (the quit flag) — truthy means "app wants to quit".

Faithful pseudocode:

```c
bool PumpMessages() {
    DAT_00838198 = 1;                           // re-entrancy guard
    tagMSG msg;
    int peek = PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE);
    while (peek != 0 && msg.message != WM_QUIT /* 0x12 */) {
        // 1st filter: IME / composition — stub returns 0 in retail
        if (FUN_006a1050(&msg) == 0) {
            // 2nd filter: Keystone hot-keys / accelerators
            if (FUN_00557a90(msg.hwnd, 0 /*defaults to HACCEL*/, &msg) == 0) {
                TranslateMessage(&msg);
                DispatchMessageA(&msg);          // routes into WndProc
            }
        }
        peek = PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE);
    }
    // Redraw fence: if someone requested a redraw while we were pumping,
    // toggle the "was-active" flag so the next frame starts fresh.
    if (DAT_00838199) {
        if (DAT_0086734c != 0 && DAT_00838197 /*cursor hidden*/) {
            if (DAT_00818b02 == 0 || DAT_00818b68 != 0)
                DAT_00818b68 = 0;
            else
                DAT_00818b68 = 1;
        }
        DAT_00838199 = 0;
    }
    DAT_00838198 = 0;
    return (bool)DAT_00838194;
}
```

### 4.1 `FUN_00557a90` — Keystone's pre-dispatch hook

```c
LRESULT Keystone_PeekMessage(HWND hwnd, HACCEL haccel, LPMSG msg) {
    if (DAT_00870c2c == NULL) return 0;           // Keystone not alive yet
    if (haccel == NULL) haccel = DAT_00870c54;    // default to our accels
    return DAT_00870c2c->vtable[0x6c](hwnd, haccel, msg);
}
```

This is why Keystone gets first shot at input — the pump checks with
Keystone *before* it calls `TranslateMessage`, so Keystone can suppress
a key-down or convert it into a menu command.

### 4.2 `FUN_006a1050` — IME hook

A 3-byte function that just returns 0. So in retail, the IME filter is a
no-op. `ImmGetContext` / `ImmAssociateContext` still happens during
Keystone creation (§5.2), so Asian-language IME works, but it's handled
inside Keystone rather than by a separate pre-dispatch filter.

### 4.3 Re-entrancy / recursion

`DAT_00838198` guards against `PumpMessages` calling itself. Nothing in
the pump body calls back into `PumpMessages` directly, but
`DispatchMessageA` can — e.g. a menu handler that runs a modal dialog
loop of its own.

---

## 5. UI system (Keystone) initialization

### 5.1 Library discovery

`FUN_00557930` at `0x00557930` (chunk_00550000.c:7017) is the
"keystone + plugins resolver". Runs once during core startup:

```c
bool Keystone_LoadDlls() {
    if (DAT_00870c30 != NULL) return 1;                  // idempotent
    if (!FUN_005577a0()) return 0;                       // msxml4.dll required

    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 != NULL) return 1;

    DAT_00870c54 = CreateAcceleratorTableA(NULL, 0);     // empty accel table
    return 0;
}
```

### 5.2 `FUN_005577a0` — msxml4 probe

```c
bool MsXml4IsAvailable() {
    hLibModule = LoadLibraryA("msxml4.dll");
    if (!hLibModule) { DAT_00870c58 = 0; return 0; }
    DAT_00870c58 = 1;
    FUN_00557e40();
    DAT_00870c58 = FUN_00557e80();    // verify registry / DLL version
    FreeLibrary(hLibModule);
    FUN_00557e60();
    return DAT_00870c58;
}
```

Keystone uses MSXML4 to parse the UI layout XML blobs packed in the
client dats (`.layout`, `.skn` files referenced elsewhere). **Our port
will need an equivalent XML deserializer if we load retail UI layouts
as-is** — but we can also bake them into code; see §8 porting notes.

### 5.3 `FUN_00557850` — `KeystoneCreate` call

```c
bool Keystone_CreateRoot() {
    if (DAT_00870340 == 0 || DAT_00870c34 == NULL) return 0;

    char cwd[0x2000];
    if (!_getcwd(cwd, 0x2000)) return 0;

    wchar_t cwdW[0x2000];
    MultiByteToWideChar(0, 0, cwd, -1, cwdW, 0x2000);

    HWND hwnd = DAT_008381a4;
    HIMC himc = ImmGetContext(hwnd);
    ImmReleaseContext(hwnd, himc);

    DAT_00870c2c = DAT_00870c34(               // KeystoneCreate(...)
        hwnd,                                   // target HWND
        DAT_00870340[0x468/4],                  // Turbine Core handle field
        cwdW,                                    // working directory
        0, 0, 0, 0);

    ImmAssociateContext(hwnd, himc);

    if (DAT_00870c2c != NULL) {
        int evtPayload = 0;
        DAT_00870c2c->vtable[0x5c](0x69, 2, &evtPayload); // focus msg
        return 1;
    }
    return 0;
}
```

The arguments passed to `KeystoneCreate` are, by experimentation and
convention:
- `HWND hwnd` — target window
- `void* coreHandle` — the engine-side handle Keystone embeds for
  callbacks (resources, input, rendering integration)
- `LPCWSTR workingDir` — where to find XML layouts & font atlases
- 4 unused `NULL` slots (reserved for callbacks, probably)

### 5.4 Keystone vtable, as observed

| Slot | Role |
|------|------|
| 0x08 | Release / destroy |
| 0x14 | FindPanel(name) |
| 0x20 | Shutdown (full) |
| 0x24 | CreatePanel(..., 4 args) |
| 0x28 | SetActiveElement(elem) |
| 0x2c | GetActiveElement() |
| 0x5c | SendEvent(type, subtype, payload) |
| 0x60 | QueryState(arg) |
| 0x6c | **WndProcFilter(hwnd, haccel, &MSG)** — the input pump's hook |

### 5.5 Fonts and other dat-based resources

Portal-dat region registration (near `chunk_00410000.c:13510`) maps the
following regions for use by Keystone:

| Name | Resource ID range | Extension |
|------|-------------------|-----------|
| `emp/property` | — | `.font` |
| `fonts` | `0x40000000..0x40000fff` | `.font` |
| `fonts_local` | `0x40001000..0x40ffffff` | `.font_local` |
| (empty name) | `0x41000000..0x41ffffff` | `StringTable` (DAT_00796760) |
| `stringtable` | `0x78000000..0x7fffffff` | `.dbpc`, `.pmat` |
| `properties` | — | — |

These arrays of FileID → filename pairs are how Keystone resolves
"font://ac_fondant_36" at runtime.

### 5.6 UI commands it registers (chat side)

The chat parser registers these user-facing UI commands
(`chunk_00570000.c:7298+`):

- `@saveui <filename>` / `@loadui <filename>` — persist layout
- `@lockui` — toggle edit-mode / lock
- `@saveautoui` / `@loadautoui` — auto-saved layout on disk

These go through `FUN_0056fae0`, the chat-command registrar. The
handlers `FUN_00570dc0` / `FUN_00570f20` / `LAB_00571180` live in
`chunk_00570000.c`. They all dispatch through `DAT_00870c2c` (the
Keystone root).

---

## 6. Frame loop structure

### 6.1 The two `PumpMessages` callers

Two functions call `FUN_00439e50`:

| Caller | Address | Purpose |
|--------|---------|---------|
| `FUN_00411630` | 0x00411630 | Main in-game frame step |
| `FUN_00411fa0` | 0x00411FA0 | Login/connect waiting-state frame step |

They share the same 7-function rendering tail. Both are called via
`FUN_004017c0` @ 0x004017C0 (the object-owned dispatch):

```c
void FUN_004017c0(int self) {
    self->vtable_at_0x118[0x20]();    // start-of-frame hook
    FUN_00411fa0();                   // or 00411630 depending on state
}
```

The difference: `FUN_00411630` is called while waiting for the world
server to respond (during login / portal transitions) and returns after
one iteration; `FUN_00411fa0` returns 1 to continue / 0 to quit.

### 6.2 Frame body (in-game)

```c
void MainFrame_InGame(App* app) {
    FUN_0040fbd0();                    // stat counters
    if (PumpMessages()) {               // quit requested?
        FUN_00543fc0();                 // shutdown ACK
        return;
    }
    FUN_00543440();                     // reconcile network state
    app->cursor->vtable[0x48]();        // cursor tick
    FUN_0045d0b0();                     // render frame (§6.3)
    FUN_004554b0();                     // game tick (§6.4)
    FUN_0043e690();                     // empty in retail — perf marker
    FUN_0043dc70();                     // mouse / input
    FUN_00455610();                     // late game-tick fixup
    FUN_0043fcd0(1);                    // flush + Present (§3)
    FUN_004392b0();                     // Sleep(~1ms) to cap at ~60 FPS
}
```

### 6.3 Render body (`FUN_0045d0b0` @ 0x0045D0B0)

```c
void RenderFrame(World* w) {
    FUN_0045a350();                         // begin scene / clear backbuffer
    FUN_004596b0();                         // draw 3D world (terrain, static objs)
    if (w->drawUnderwaterFx)
        FUN_0045cde0();                     // water / underwater surface
    FUN_0045b7c0();                         // draw alpha / particles / billboards
    FUN_0045b4c0(3, 0);                     // switch to orthographic / 2D
    if (DAT_00837ff4 != NULL)
        DAT_00837ff4->vtable[0x10]();       // device BeginScene for 2D
    FUN_0045b900();                         // draw Keystone UI as 2D quads
}
```

`FUN_0045b900` walks the list of on-screen UI elements
(`DAT_00870340->field_0x9c + 0xb4`, a linked list of panels) and calls
`FUN_0045b8a0` on each, which in turn calls `FUN_0045ad80` (the per-panel
draw routine) and recurses through children via
`FUN_00464110` / `FUN_00464490` (first-child / next-sibling iterators).

### 6.4 Game tick (`FUN_004554b0`)

```c
void GameTick(World* w) {
    if (!w->frozen) {
        if (!w->sendingLogout && FUN_00455d00())
            FUN_00455830();
        if (w->chat != NULL && w->chat->queueLen > 0)
            FUN_00455ad0(w->chat + 0x48, 0);  // flush chat buffer

        FUN_00509480();                       // physics engine step
        FUN_0050a420();                       // AI / server-sync
        if (DAT_008ee9c8) {                   // playing cinematic?
            FUN_005a7800();
            FUN_005062e0();
        }
        FUN_005524a0();                       // scripts
    } else {
        FUN_00455d00();                       // frozen: minimal state
    }
    FUN_0043f7b0();                           // frame post-flush

    // drain main-thread command queue
    while (w->deferredQueue != NULL) {
        ptr = w->deferredQueue->head;
        if (!w->vtable[1](ptr)) FUN_00453a20(ptr);
        else                    w->vtable[2](ptr);
        InterlockedDecrement(ptr->refcount);
    }

    if (!w->minimized)
        w->renderer->vtable[0xa8]();          // sync render state

    FUN_0054d700();                           // misc cleanup
}
```

So the tick order within one frame is:

```
Pump → {Render begin → draw 3D → draw UI → Render end/Present} → Game state → Sleep
```

Which is unusual — rendering happens BEFORE the game tick, not after.
The reason is that the rendering pass reads a *stable* snapshot of the
world built at the end of the previous frame; the current tick then
builds the snapshot for next frame. This is a double-buffered simulation
state pattern. **Our port should mirror this.**

---

## 7. Input dispatch wiring

Two layers.

### 7.1 Win32 → App bridge

`FUN_00439240` @ 0x00439240 (chunk_00430000.c:7780):

```c
int Renderer_FilterMessage(UINT msg, WPARAM wp, LPARAM lp, HWND hwnd, int* handled) {
    if (DAT_00838196 && DAT_00837ff4) {
        MSG m = { msg, wp, lp, hwnd, GetMessageTime(), ... };
        return DAT_00837ff4->vtable[0x70](&m, handled);
    }
    *handled = 0;
    return 0;
}
```

This is the post-Keystone, pre-DefWindowProc filter called from the
WndProc. It lets the renderer eat WM_SIZE / WM_DISPLAYCHANGE etc.

### 7.2 UI mouse hit-test

`FUN_00689890` @ 0x00689890 (chunk_00680000.c) is the `MouseCursor`
object's `TryHit` method — it builds a rect around the cursor (`InflateRect`),
asks `PtInRect`, and routes to `FUN_00689520` which ultimately posts
hover / click events into the Keystone event queue. This is our
reference for "UI click dispatch order": `cursor → drag check → drop
target check → hover → click`.

### 7.3 Cursor management

Three small helpers manage cursor visibility / default arrow:

- `FUN_00439320` @ 0x00439320 — "hide cursor" (clears DAT_00838197)
- `FUN_00439400` @ 0x00439400 — "show cursor" + fall back to
  `LoadCursorA(NULL, IDC_ARROW /*0x7f00*/)`
- `FUN_004392f0` @ 0x004392F0 — "confine cursor to client" via
  `DAT_00837ff4->vtable[0x74](1)`

**Retail behavior quirk:** the hardware cursor is replaced by a
software-drawn cursor as soon as the UI loads. The hardware `IDC_ARROW`
is only visible during the launcher → window transition. Keystone owns
cursor icons; `DAT_00870340->field_0x10 != 0` means "UI is drawing a
cursor", and the Win32 cursor is suppressed.

---

## 8. C# port shape (no code yet — just the architecture contract)

The retail layout maps cleanly onto Silk.NET if we keep the four-object
factoring:

| Retail global | Proposed C# class | Owns |
|---------------|-------------------|------|
| `DAT_008381a4` (HWND) | `AcDream.App.Rendering.GameWindow` (exists) | Silk window |
| `DAT_0086734c` | `AcDream.App.Rendering.Driver.GraphicsDriverFactory` | GL backend selection, probes |
| `DAT_00870340` | `AcDream.App.Rendering.TurbineCore` | dat sources, GL device, font atlases, Keystone handle |
| `DAT_00837ff4` | `AcDream.App.Rendering.RenderDevice` | per-frame GL state, 2D+3D camera |
| `DAT_00870c2c` | `AcDream.App.UI.KeystoneRoot` | UI panels, layout, input dispatch |

### 8.1 Where our `GameWindow.cs` fits

Our existing `GameWindow.cs` is roughly the union of `TurbineWndProc`
(implicit — Silk forwards Win32 for us) + `CreateMainWindow` +
`MainFrame_InGame`. The structure:

```
GameWindow.Run()            = WinMain body + vtable[0x1c] (App::Run) combined
    window.Load   = OnLoad  = FUN_0054e1a0 (core + post-device init)
    window.Update = OnUpdate = FUN_004554b0 (game tick)
    window.Render = OnRender = FUN_0045d0b0 + FUN_0043fcd0 (render + present)
```

We currently have no direct equivalent of `PumpMessages` because Silk.NET
pumps messages inside `window.Run()` and fires synthetic events. That's
fine — but we still need to preserve the **ordering invariant** from
§6.4: the render pass reads a snapshot built by the previous tick. In
Silk.NET terms: `OnUpdate` and `OnRender` are given distinct `dt`s and
our simulation state must be double-buffered. Look at our code — we
currently do physics inside OnUpdate (late), which is fine; the render
pass reads the updated state. This matches retail.

### 8.2 Proposed UI scaffolding (what to add next)

1. **`AcDream.App.UI.UIHost`** — one per-GameWindow. Owns a stack of
   panels, a `Font` cache, an input dispatcher, and a draw pass.
   Methods roughly matching Keystone vtable:
   - `OpenPanel(name)` ↔ `Keystone::CreatePanel` (slot 0x24)
   - `FindPanel(name)` ↔ slot 0x14
   - `SetFocus(elem)` ↔ slot 0x28
   - `DispatchWin32Message(...)` ↔ slot 0x6c
   - `SendEvent(type, subtype, payload)` ↔ slot 0x5c
   - `DrawFrame(deltaSeconds)` ↔ called from `OnRender` after 3D world
2. **`AcDream.App.UI.Panel` / `Widget`** — retain-mode tree with
   first-child / next-sibling iterators matching `FUN_00464110` /
   `FUN_00464490`. Each has `Draw(gl, spriteBatch)` and `HitTest(pt)`.
3. **Input interception** — our existing `_input.Mice` / `_input.Keyboards`
   handlers in `GameWindow.OnLoad` should call `UIHost.DispatchMouseMove`
   *first*, and only move the camera if the UI didn't claim the event.
   This matches §1.4's "Keystone first, renderer second, Def last".
4. **Font and texture loading** — Keystone pulled `.font` from dat
   region `0x40000000..0x40000FFF`. Our port already has `DatCollection`;
   add a `FontDat` resolver that reads the same region and emits a
   Silk.NET-usable texture atlas.
5. **Layout format** — retail stores XML layouts parsed via MSXML4.
   We don't want to ship an XML interpreter for the MVP. Plan: start
   with code-defined panels (login form, world view HUD) and deserialize
   XML in a later milestone. Keystone XML schemas are documented in
   `references/holtburger/` (briefly) and in some old Turbine docs — not
   an R1 concern.

### 8.3 Two pumps vs one

Retail has `FUN_00411630` (wait-on-server) and `FUN_00411fa0` (in-world).
Our Silk pump is unconditional; the state machine is in
`StreamingController` + `PlayerController`. The behavioral difference
only matters for "are we in a modal loading screen" — we can model that
as a Boolean flag and skip the game-tick portion of OnUpdate when it's
set.

### 8.4 Quitting cleanly

- `DAT_00838194 = 1` is set by `FUN_00439230` @ 0x00439230 (a one-liner
  that flips the flag). That's the equivalent of our `Window.Close()`.
- The WM_QUIT (0x12) check in the pump is also triggered by the OS on
  shutdown. Silk handles WM_QUIT internally.
- Shutdown order in retail is: `FUN_00543fc0` (server goodbye) →
  vtable[0x2c] (app shutdown) → teardown singletons → `FUN_00406f90`.
  Our `OnClosing` handler should do the same: network-flush, dispose
  physics, dispose renderers, dispose dats.

### 8.5 Cross-validation with reference repos

- **ACViewer / WorldBuilder**: both are MonoGame / Silk.NET *viewers* —
  neither has a retail-style multi-window bring-up or a Keystone layer.
  We cannot borrow UI code from them, only dat-decoding helpers.
- **ACE**: server only. No WndProc / no UI. Irrelevant to this slice.
- **holtburger**: Rust TUI client. Confirms the overall life-cycle
  (login → post-login → world frames) but its "UI" is Ratatui — not
  comparable.
- **AC2D**: C++ client demo. Its `cInterface.cpp` confirms that the
  retail cursor / WndProc flow exists but is much simpler there (no
  Keystone — AC2D draws its UI directly). Useful only as a sanity
  check that our frame order (pump → render → tick → sleep) is the
  right shape.

All of this confirms one important conclusion: **there is no existing
reference repo that replicates Keystone**. That part of the port is
genuinely novel. Plan the UI work as an internal `AcDream.App.UI`
module from scratch, not as a port.

---

## 9. Lookup tables for future sub-agents

Master table of every FUN_ / DAT_ referenced above:

| Symbol | Addr | File | Role |
|--------|------|------|------|
| WinMain (renamed) | 0x004013A0 | chunk_00400000.c | app entry |
| `FUN_00401700` | 0x00401700 | chunk_00400000.c | string copy helper |
| `FUN_004017c0` | 0x004017C0 | chunk_00400000.c | wraps frame call |
| `FUN_00412180` | 0x00412180 | chunk_00410000.c | App::Initialize |
| `FUN_00411630` | 0x00411630 | chunk_00410000.c | pre-login frame step |
| `FUN_00411fa0` | 0x00411FA0 | chunk_00410000.c | main frame step |
| `FUN_00439140` | 0x00439140 | chunk_00430000.c | default window size 800×600 |
| `FUN_00439230` | 0x00439230 | chunk_00430000.c | **QuitApp** (sets DAT_00838194) |
| `FUN_00439240` | 0x00439240 | chunk_00430000.c | renderer msg filter |
| `FUN_00439320` | 0x00439320 | chunk_00430000.c | HideCursor |
| `FUN_00439370` | 0x00439370 | chunk_00430000.c | sanitize-window-size |
| `FUN_00439400` | 0x00439400 | chunk_00430000.c | ShowCursor |
| `FUN_0043985D`-ish `LAB_00439860` | 0x00439860 | chunk_00430000.c | **WndProc** (not decompiled) |
| `FUN_00439e50` | 0x00439E50 | chunk_00430000.c | **PumpMessages** |
| CreateMainWindow (renamed) | 0x0043BA60 | chunk_00430000.c | window + device bring-up |
| `FUN_0043ac60` | 0x0043AC60 | chunk_00430000.c | renderer init dispatcher |
| `FUN_0043ad90` | 0x0043AD90 | chunk_00430000.c | renderer init wrapper |
| `FUN_0043fcd0` | 0x0043FCD0 | chunk_00430000.c | **flush + Present** |
| `FUN_004554b0` | 0x004554B0 | chunk_00450000.c | **GameTick** |
| `FUN_0045d0b0` | 0x0045D0B0 | chunk_00450000.c | **RenderFrame** |
| `FUN_0045a350` | 0x0045A350 | chunk_00450000.c | begin scene |
| `FUN_004596b0` | 0x004596B0 | chunk_00450000.c | draw 3D world |
| `FUN_0045b7c0` | 0x0045B7C0 | chunk_00450000.c | draw alpha pass |
| `FUN_0045b900` | 0x0045B900 | chunk_00450000.c | draw UI / 2D pass |
| `FUN_0054d0c0` | 0x0054D0C0 | chunk_00540000.c | build driver factory |
| `FUN_0054d110` | 0x0054D110 | chunk_00540000.c | build Turbine Core |
| `FUN_0054e1a0` | 0x0054E1A0 | chunk_00540000.c | post-device wiring |
| `FUN_00557850` | 0x00557850 | chunk_00550000.c | **KeystoneCreate call site** |
| `FUN_00557930` | 0x00557930 | chunk_00550000.c | Keystone + plugin DLL load |
| `FUN_00557a90` | 0x00557A90 | chunk_00550000.c | Keystone pre-dispatch filter |
| `FUN_005577a0` | 0x005577A0 | chunk_00550000.c | MSXML4 probe |
| `FUN_006895d0` | 0x006895D0 | chunk_00680000.c | alloc render device |
| `FUN_00689890` | 0x00689890 | chunk_00680000.c | mouse hit-test dispatch |
| `DAT_008381a4` | 0x008381A4 | — | HWND |
| `DAT_0086734c` | 0x0086734C | — | graphics factory |
| `DAT_00870340` | 0x00870340 | — | Turbine Core |
| `DAT_00837ff4` | 0x00837FF4 | — | render device |
| `DAT_00870c30` | 0x00870C30 | — | keystone.dll HMODULE |
| `DAT_00870c34` | 0x00870C34 | — | KeystoneCreate fn ptr |
| `DAT_00870c2c` | 0x00870C2C | — | Keystone root |
| `DAT_00870c54` | 0x00870C54 | — | HACCEL |
| `DAT_00838194` | 0x00838194 | — | Quit flag |

---

## 10. Open questions for follow-up agents

1. **WndProc body** — Ghidra skipped 0x00439860. A raw-bytes dump
   (not yet in our decompiled set) would let us confirm the
   Keystone-then-renderer-then-DefWindowProc ordering exactly.
2. **`KeystoneCreate` signature** — we infer the 7 args from the call
   site, but the 4 trailing NULL slots may be (pfnRender, pfnInput,
   pfnResourceResolve, pfnCommand) callbacks. Dumping
   `PTR_FUN_008000c8`'s surrounding strings in `chunk_00800000.c` (RDATA)
   should reveal Keystone's export list.
3. **`FUN_0045b900` draw pass** — we haven't enumerated the full Widget
   vtable. Slots [0xb0], [0xb4] (enable/disable hook) and [0xbc] through
   [0xf8] appear repeatedly elsewhere; a dedicated sub-agent should map
   them.
4. **Panel registration order** — we know `@saveui` / `@loadui` persist
   layouts but not the list of well-known panel names (`chat_window`,
   `compass`, `hp_bar`, etc.) the default UI creates. Grep
   `chunk_00570000.c` / `chunk_00580000.c` for string constants like
   `"panel_"` or `"pnl_"` to enumerate.
5. **Font atlas format** — `.font` files live in dat regions
   `0x40000000..0x40000FFF`; their binary layout is in `DatReaderWriter`
   already, but we haven't validated rendering round-trip.

End of document.
