acdream/docs/research/retail-ui/00-master-synthesis.md
Erik 55aaca7a14 feat(ui): Phase D.2a — VitalsPanel wired into GameWindow + backend pivot
Closes Phase D.2a. Launch with ACDREAM_DEVTOOLS=1 now shows a live
ImGui "Vitals" window whose HP bar reads CombatState.GetHealthPercent
for the local player. Without the env var the branches are dead code,
no ImGui context is created, and behaviour is identical to before.

GameWindow hunks:
  - fields: _imguiBootstrap / _panelHost / _vitalsVm + DevToolsEnabled
  - init (OnLoad): construct bootstrap + host, register VitalsPanel
  - GUID push: _vitalsVm?.SetLocalPlayerGuid(chosen.Id) at live-connect
  - frame begin: _imguiBootstrap.BeginFrame(dt) after GL clear
  - frame end: _panelHost.RenderAll(ctx) + _imguiBootstrap.Render() after debug overlay
  - input gating: skip WASD when ImGui.GetIO().WantCaptureKeyboard

Backend pivot: Hexa.NET.ImGui → ImGui.NET + Silk.NET.OpenGL.Extensions.ImGui.

First-light integration with the Hexa backend crashed 0xC0000005 inside
Hexa.NET.ImGui.Backends.OpenGL3.ImGuiImplOpenGL3.InitNative. Root cause:
Hexa's native OpenGL3 backend resolves GL function pointers via GLFW or
SDL internally; with Silk.NET (which uses neither) the pointers are null
and the native code crashes on first use. The mitigation path was
already planned — the design doc's Risk section called a pivot to
ImGui.NET a "one-morning operation" — and that's exactly what happened.

  - Packages: Hexa.NET.ImGui 2.2.9 + Hexa.NET.ImGui.Backends 1.0.18
    → ImGui.NET 1.91.6.1 + Silk.NET.OpenGL.Extensions.ImGui 2.23.0
  - ImGuiBootstrapper: was static Initialize(gl)+Shutdown() wrapping
    Hexa's OpenGL3 init; now an IDisposable wrapping Silk.NET's
    ImGuiController instance which handles GL backend init + input
    subscription in one go.
  - SilkInputBridge.cs deleted (~190 LOC): ImGuiController subscribes
    IKeyboard / IMouse events itself, we don't need a bespoke bridge.
  - ImGuiPanelRenderer: ImGuiNET.ImGui.* calls instead of
    Hexa.NET.ImGui.ImGui.*. Widget surface unchanged.

Boundary discipline is preserved — no panel imports ImGuiNET; only
ImGuiPanelRenderer does. The D.2b custom toolkit will implement the
same IPanelRenderer contract without touching panel code.

Out of scope (tracked for follow-up):
  - Stam/Mana currently return float? null (VitalsVM). Absolute values
    need LocalPlayerState + PlayerDescription (0x0013) parsing to be
    stored rather than discarded — filed as a post-D.2a issue.
  - Mouse-capture gating (WorldMouseFallThrough-style click-through
    tests) — not needed until we add clickable inventory items.

Roadmap + memory + architecture doc + UI framework plan updated in the
same commit per CLAUDE.md roadmap-discipline rules. 753 tests pass
(550 Core + 192 Core.Net + 11 new UI.Abstractions), 0 build warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 00:43:46 +02:00

31 KiB
Raw Blame History

Retail AC Client GUI — Master Synthesis

Scope note (2026-04-24, updated 2026-04-25): This document describes retail's Keystone UI toolkit — it is the research foundation for Phase D.2b (custom retail-look backend), not Phase D.2a (shipped ImGui scaffold, AcDream.UI.Abstractions + ImGui.NET + Silk.NET.OpenGL.Extensions.ImGui + VitalsPanel). When reading this for implementation guidance, assume D.2a has shipped a working AcDream.UI.Abstractions layer (IPanel, IPanelRenderer, ViewModels, Commands) and you are building the custom retained-mode toolkit that implements the same contracts using dat-sourced fonts / sprites / cursors. See docs/plans/2026-04-24-ui-framework.md for the staged UI strategy and memory/project_ui_architecture.md for the one-page crib-sheet.

Date: 2026-04-17 Sources: 6 parallel Opus research passes over the decompiled acclient.exe (22,225 functions / 688K lines). Individual deep dives:

This document is the combined, cross-referenced picture of how the retail AC client renders and drives its UI, plus the port plan for acdream.


0. TL;DR — single most important finding

The retail UI widget toolkit is not in acclient.exe. The client loads keystone.dll (plus two plugins ACHelpPlugin.dll, ACPluginManager.dll) at startup via LoadLibraryA, gets a single COM-like interface pointer (DAT_00870c2c), and delegates all widget layout, hit-testing, input, and drawing to Keystone. The client supplies primitives Keystone consumes (CFont, CSurface, CString, the D3D device) and routes commands by resource ID (0x10000000+ custom event IDs, 0x21xxxxxx LayoutDesc IDs, 0x06xxxxxx sprites, 0x40000xxx fonts).

Implication for acdream: we cannot "port a C++ widget hierarchy from the decompile" — there is none. We implement our own retained-mode toolkit on top of Silk.NET OpenGL, consuming the same dat-borne textures and fonts so the UI is visually identical to retail even though the class hierarchy underneath is new. This is the same approach AC2D took. Section 11 below describes the C# layout.


1. Process / frame architecture (from slice 01)

WinMain @ 0x004013A0                                 (chunk_00400000.c)
  └─ App::Initialize (vtable[0x10])
        ├─ dat cache init, string-table init
        ├─ vtable[0x74] "CreateMainWindow"
        │     ├─ RegisterClassA("Turbine Device Class")
        │     ├─ CreateWindowExA(..) → DAT_008381a4 (HWND)
        │     ├─ graphics factory       → DAT_0086734c
        │     ├─ render device          → DAT_00837ff4   (lightweight)
        │     ├─ Turbine Core           → DAT_00870340   (GL surfaces + dats)
        │     └─ Keystone bring-up:
        │           LoadLibraryA("keystone.dll")       → DAT_00870c30
        │           LoadLibraryA("plugins\\ACHelpPlugin.dll")
        │           LoadLibraryA("plugins\\ACPluginManager.dll")
        │           GetProcAddress "KeystoneCreate"    → DAT_00870c34
        │           KeystoneCreate(hwnd, core, cwd, 0,0,0,0) → DAT_00870c2c
        │
        ├─ App::Run (vtable[0x1c])           — main loop forever
        │     while (!quit) {
        │         PumpMessages @ 0x00439E50          // FUN_00439e50
        │             PeekMessageA
        │             IME filter (stub)
        │             Keystone->TranslateAccelerator (vtable +0x6c)
        │             TranslateMessage + DispatchMessageA → WndProc @ 0x00439860
        │         RenderFrame @ 0x0045D0B0           // 3D world + UI + overlays
        │         GameTick    @ 0x004554B0           // physics + AI + scripts
        │         FlushPresent @ 0x0043FCD0          // clear + begin + UI + end + Present
        │         Sleep(~1ms)
        │     }
        └─ App::Shutdown (vtable[0x2c])

Frame body ordering (important): render → tick → Sleep. Retail renders BEFORE the game tick, because the render pass reads a stable snapshot built at the end of the previous frame (double-buffered simulation state). acdream already follows this pattern.

Global state block (acdream should replicate the 4-way factoring):

Retail global Role Proposed C# class
DAT_008381a4 HWND (Silk.NET IWindow)
DAT_0086734c Graphics factory (handled by Silk.NET GL selection)
DAT_00870340 Turbine Core (GL + dats + fonts) AcDream.App.UI.UiHost
DAT_00837ff4 Render/input device AcDream.App.UI.UiDevice
DAT_00870c2c Keystone root (widgets) AcDream.App.UI.UiRoot (our toolkit)

2. Rendering pipeline (from slice 03)

2.1 Top-level frame

FUN_0043fcd0 SceneTool::RenderFrame
├─ RenderDevice::Clear(color|depth)
├─ if world.visible:
│     Scene::BeginFrame
│     Scene::RenderWorld        (terrain, statics, lights)
├─ if DAT_0083846c != 0: FUN_005da8f0()   <-- UI PANEL TREE WALK (7.8 KB func)
├─ if DAT_00838468 != 0: FUN_00692470()   <-- CHAT TEXT + EDIT LINE
├─ FUN_0043f7f0()                         <-- DEBUG/STATS HUD
├─ RenderDevice::Clear(color only)
├─ RenderDevice::BeginScene
├─ RenderDevice::EndScene + Present
└─ FUN_0043e6b0()                         <-- frame stats

2.2 Single sprite / text quad

  • Vertex format: 24 bytes = (x, y, z=0, rhw, u, v) — XYZRHW + TEX1
  • No per-vertex color; color is set via D3DRS_TEXTUREFACTOR per draw
  • 6 verts per quad (TL, BL, BR, BR, TR, TL — CCW with D3D conventions)
  • NDC conversion applies a DX9-style half-pixel offset
  • Global buffer at DAT_008f9a90..8f9a98 (base, capacity, write-offset)
  • Hot loop unrolled 4 glyphs per iteration
  • Flush: FUN_006974d0FUN_005a26d0(D3DPT_TRIANGLELIST, verts/3, pV, 0x142, tex, tex, stride=24)

2.3 Fonts

  • DBObj type Font at IDs 0x40000000..0x40000FFF (portal.dat)
  • On-disk FontCharDesc per glyph (9 bytes: codepoint, offsetX/Y, w, h, kern-before/after, vertOffset)
  • References a RenderSurface (0x06xxxxxx) for the source pixel sheet
  • Runtime baking (FUN_00697140) builds a 256×256 atlas from the source surface, clamps to printable ASCII 0x20-0x7E (or wider if present), and caches glyph UVs + metrics as 24-byte runtime records
  • No inline color escapes. Chat colors arrive as full ARGB dwords on the wire in ChatText/ChannelBroadcast messages

2.4 UI textures

ID range DBObjType Role
0x06000000..0x06FFFFFF RenderSurface All button images, panel backgrounds, icons, cursor bitmaps, font glyph sheets
0x08000000..0x08FFFFFF SurfaceTexture Wraps a RenderSurface with palette/tiling info
0x0F000000..0x0FFFFFFF SurfaceMaterial Materials referencing SurfaceTextures
0x40000000..0x40000FFF Font Font metadata — references a RenderSurface
0x21xxxxxx LayoutDesc Panel layouts — tree of ElementDescs + StateDescs
0x23/0x24xxxxxx StringTable Localized UI strings
0x14xxxxxx MasterInputMap Default keybinds
0x41xxxxxx LanguageInfo Localization root

2.5 Blend + depth + clipping

  • Blend: SRC_ALPHA / INV_SRC_ALPHA ("over")
  • Depth: off for UI
  • Clipping: retail mostly pre-clips at the vertex-generation stage; true hardware scissor is used in some panel interiors (chat scroll, inventory grid)

3. Class hierarchy / polymorphism (from slice 02)

3.1 What is in acclient.exe

Class Vtable Role
CUIManager (singleton) PTR_FUN_00799fc4 DAT_00838374; AddListener/RemoveListener at vtable +0x0c
CUIListener (embedded base) PTR_FUN_00801670 Any object wanting UI events embeds this as MI secondary base
CFont (chunk_00440000.c) Glyph lookup, range map, atlas blit
CSurface PTR_FUN_0079c26c 40-byte bitmap wrapper (w, h, bpp, pixels)
CString helpers FUN_00402490/07e40/0b8f0 Refcounted wide-string buffer (string builder)

3.2 The Keystone interface (DAT_00870c2c)

Observed vtable slots (from call sites in acclient.exe):

Slot Role
+0x08 Release / destructor
+0x14 FindPlugin(wchar_t* name)
+0x20 ProcessFrame — called once per frame from FUN_0043fcd0
+0x24 CreateOrActivate(a, b, c, d) — 4-arg plugin entry
+0x28 ClearActive(0)
+0x2c GetActive
+0x5c SendCommand(id, op, buffer) — generic command dispatch
+0x60 HitTest(POINT*)
+0x6c TranslateAccelerator(hwnd, haccel, &MSG) — pump's pre-filter

3.3 Polymorphism pattern

Classic MSVC multiple inheritance with per-base-class vtables. A widget like CharGen has four vtables at fixed offsets inside the object (+0x00, +0x04, +0x08, +0x12C). AddListener is passed the INNER this pointer (the listener subobject), not the outer.

3.4 Port decision

acdream implements its own retained-mode widget toolkit. Keystone is not decompiled here and reverse-engineering it is out of scope. We build UiElement / UiPanel / UiButton / UiLabel / UiEditBox / etc. with virtual OnDraw / OnHitTest / OnMouseMessage / OnKeyMessagenot multiple inheritance.


4. Input / event routing (from slice 04)

4.1 Message pump

char FUN_00439e50(void) {                    // 0x00439E50
    DAT_00838198 = 1;
    MSG msg;
    while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE) && msg.message != WM_QUIT) {
        if (FUN_006a1050(&msg))    continue;                   // IME filter (stub)
        if (FUN_00557a90(msg.hwnd, 0, &msg)) continue;         // Keystone accel
        TranslateMessage(&msg);
        DispatchMessageA(&msg);    // → WndProc at 0x00439860
    }
    DAT_00838198 = 0;
    return DAT_00838194;   // quit flag
}

4.2 WndProc

Ghidra did not decompile LAB_00439860 but its behavior is reconstructed from dispatch sites:

  • Win32 WM_* numbers are reused as internal event type codes (0x200 mouse-move, 0x201 LDown, 0x202 LUp, 0x204/205 RDown/Up, etc.)
  • All WM_* forwarded to DAT_00837ff4 (the Device)'s vtable methods

4.3 Device object (DAT_00837ff4) vtable

Slot Method
+0x04 AttachWindow(HWND)
+0x10 BeginScene
+0x18 GetMouseX
+0x1C GetMouseY
+0x34 RegisterTimerEvent(eventType, target, delayMs) — tooltip delays
+0x38 FireEvent(eventType, target)
+0x44 GetKeyboardFocus
+0x48 SetCapture(widget, kind)
+0x4C ReleaseCapture(kind)
+0x6C TranslateAccelerator
+0x70 WndProc filter — (MSG*, bool* handled)
+0x74 SetDragCursor(bool)
+0x78 ResetDragDrop

4.4 Event struct — delivered to widget vtable +0x128

struct Event {
    int      source_id;        // widget event id (0x10000000+ app range)
    Widget*  target_widget;
    int      event_type;       // see table below
    int      data0, data1, data2, data3;
}

Widget vtable +0x128 = OnEvent(Event*).

4.5 Event type catalog

Code Meaning Notes
0x01 Click (left-up-over)
0x05 Hover enter
0x06 Hover leave
0x07 Tooltip (delayed, ~1000ms) Target registers via Device::RegisterTimerEvent(7, self, ms)
0x08 Double-click
0x0A Scroll-wheel data0 = wheel delta
0x0E Right-click
0x15 Drag begin Sent to source on motion > ~3px
0x1C Drag-over (continuous)
0x21 Drag entered target
0x3E Drop released data3 = accepted?
0x201 Mouse button down (raw)
0x202 Mouse button up (raw)
0x200 Mouse move
0x100 Key down data0 = VK
0x101 Key up
0x102 Char typed

4.6 Drag-drop

Single-drag-at-a-time. The Device holds DragSource + DragPayload; the event struct carries only IDs. Targets ask the Device for the current payload. Drag threshold ~3px from button-down to promote.

4.7 Modality

One modal_overlay slot on the Device. Clicks outside the modal's rect are dropped. All other panels are drawn dimmed (gray quad underneath). Login screen is the first modal.

4.8 Focus / hotkeys

No Win32 accelerator table. CreateAcceleratorTableA called with empty table. All hotkeys done in-widget:

  • Focus widget (or top panel if no focus) gets first shot at WM_KEYDOWN
  • If not consumed, walk top-level panels in z-order
  • Finally, global hotkeys (1-0 quickbar, B/I/C panel-open, etc.)
  • Chat-typing mode checks focus: if focus is in EditBox, suppress hotkeys

5. Panel inventory (from slice 05)

Summarized mapping of retail panels to the decompiled code:

Panel Chunk Key function / evidence
Chat window chunk_00570000.c Channel bitmask + ChatMessageType 0x00-0x1F + 7 wire opcodes
Character Attributes chunk_00470000.c FUN_0047ba70 — 10 attribute rows, 4 skill categories
Skills (same as Attributes) Embedded in attribute sheet
Spellbar / Spellbook chunk_004C0000.c FUN_004C7700-004C7F00 — 8 tabs × 7 slots
Paperdoll / Equipment chunk_004A0000.c FUN_004A5200 — 25 slots at offsets +0x604..+0x664
Inventory chunk_004A0000/05C0000 Burden system + error string catalog
Quickbar (light coverage) 1-10 hotkey slots
Fellowship (GameEventFellowshipFullUpdate) Member list + XP share
Allegiance (Allegiance messages) Patron/vassal tree
Character Select (Login flow) Character slots + create-new
Login First modal
Options chunk_00400000.c (partial) 55 CharacterOption bits

5.1 Chat channels (32-bit bitmask)

Full channel enum: Abuse (0x1), Admin (0x2), Audit (0x4), Advocate1-3, QA1/2, Debug, Sentinel, Help, Fellow (0x800), Vassals (0x1000), Patron (0x2000), Monarch (0x4000), 8 Society town channels, CoVassals, AllegianceBroadcast, FellowBroadcast, 3 Society groups, Olthoi.

5.2 Chat message types (color / filter enum)

0x00 Broadcast (default), 0x02 Speech, 0x03 Tell, 0x04 OutgoingTell, 0x05 System (red), 0x06 Combat, 0x07 Magic, 0x08 Channel (pink), 0x0A Social (yellow), 0x0C Emote, 0x0D Advancement, 0x0E Abuse, 0x12 Allegiance, 0x13 Fellowship, 0x14 WorldBroadcast (green), 0x15 CombatEnemy (red), 0x16 CombatSelf (pink), 0x18 Craft, 0x19 Salvaging (green), 0x1F AdminTell.

5.3 Paperdoll slot offsets

25 equipment slots at offsets +0x604 through +0x664 (4 bytes apart) in the paperdoll panel's this. Correlated with the EquipMask bit layout.

5.4 Cross-panel primitives identified

These recur across multiple panels and should be extracted as shared C# helpers:

  • WidgetLookup — lookup by resource ID (FUN_00463c00(0x1000xxxx))
  • RichTextBuilder — stream-append wide-string + format tokens
  • TooltipBuilderFUN_0042dc80 open → content → FUN_0042e590 close
  • DragDropRouter — routes 0x15 / 0x1C / 0x21 / 0x3E chains
  • RowList<T> — used by skills, spells, fellowship members, allegiance tree
  • PropertyUpdateBus — dispatches GameMessagePrivateUpdate* into bound widgets

6. HUD + dat-asset catalog (from slice 06)

6.1 HUD elements

Element Rendering technique Retail dat IDs (from AC2D)
Vital orbs (3) Textured quad + scissor rect for fill fraction; not gradients 0x060013B2 (vitals icon)
Radar Polar plot with blips; 1.18 * range shrink factor; player arrow center 0x06001388, 0x06004CC1 (bezel)
Compass strip Scrolling horizontal texture; U-offset = heading_deg / 360
Minimap Larger radar variant 0x06001065
Quickbar 10 slots; hotkey = 1-0 0x06001AB2 (slot)
Target health bar Plate appears over selected creature
Damage floaters Text rising above head for N seconds
Announcement strip Center-top for big events
World hover name Text over a 3D object under the cursor

6.2 UI dat type catalog (authoritative; verified from chunk_00410000.c range dispatcher)

ID range DBObjType Size Role
0x04xxxxxx Palette var 256-entry BGRA8 palettes
0x05xxxxxx PaletteSet var Tintable palette groups
0x06xxxxxx..0x07xxxxxx RenderSurface var All UI images (buttons, icons, backgrounds, glyph sheets, cursors)
0x08xxxxxx Surface var Extended surface with palette ref
0x0F000000..0x0FFFFFFF SurfaceMaterial var Materials
0x14xxxxxx MasterInputMap var Default keybind table
0x21xxxxxx LayoutDesc var Panel layouts (tree of ElementDesc + StateDesc + Media)
0x23-0x24xxxxxx StringTable var Localized strings
0x40000000..0x40000FFF Font var Font metadata + glyph records
0x41xxxxxx LanguageInfo var Localization root

6.3 Vital update wire messages

  • GameMessagePrivateUpdateAttribute (0x02E3) — 21 bytes per attribute
  • GameMessagePrivateUpdateVital (0x02E7) — 25 bytes
  • GameMessagePrivateUpdateAttribute2ndLevel (0x02E9) — max-value refresh
  • GameEventPlayerDescription (0xF7B0/0x0013) — initial full dump at login

VitalId enum: 0x01 MaxHealth, 0x03 MaxStamina, 0x05 MaxMana CurVitalId enum: 0x02 CurHealth, 0x04 CurStamina, 0x06 CurMana

6.4 Cursor

  • Default: LoadCursorA(NULL, IDC_ARROW=0x7F00)
  • Custom cursors: built from RGBA dat bitmap via GDI path (FUN_0043c1c0 / FUN_00439c70) → HCURSOR via CreateIconIndirect
  • Rendering mode: OS cursor suppressed when UI draws its own sprite
  • SetCursorPos used in mouse-look mode to recenter

7. Proposed C# port structure

7.1 Namespace + project layout

src/AcDream.App/UI/
   UiRoot.cs          // top-of-tree, implements IDevice, called from GameWindow
   UiElement.cs       // base widget class
   UiPanel.cs         // container with children
   UiLabel.cs         // text
   UiButton.cs        // clickable
   UiEditBox.cs       // text entry
   UiScrollBar.cs
   UiList.cs          // scrollable list
   UiImage.cs         // sprite
   UiWindow.cs        // movable / closable frame
   AcFont.cs          // dat-sourced font (portal.dat Font DBObj)
   FontCache.cs       // lazy-load font atlases
   UiSpriteBatch.cs   // 2D batched quad renderer (replaces our debug TextRenderer)
   UiRenderer.cs      // per-frame UI orchestrator — walks tree, emits draws
   UiEvent.cs         // event struct matching retail's 24-byte payload
   CursorManager.cs   // OS + dat cursor management

src/AcDream.App/UI/Panels/
   ChatWindow.cs
   AttributesPanel.cs
   SkillsPanel.cs
   SpellPanel.cs
   PaperdollPanel.cs
   InventoryPanel.cs
   QuickbarPanel.cs
   FellowshipPanel.cs
   AllegiancePanel.cs
   LoginPanel.cs
   CharacterSelectPanel.cs

src/AcDream.App/UI/Hud/
   VitalOrbs.cs
   RadarPanel.cs
   CompassStrip.cs
   SelectionTarget.cs
   DamageFloaters.cs
   AnnouncementStrip.cs
   WorldHoverName.cs

7.2 Scaffold to ship in this session

Minimum viable scaffold matching the research findings:

  1. UiEvent struct + event type enum (24 bytes, matches retail)
  2. UiElement abstract base with virtual Draw/OnMouse/OnKey/OnTick/OnHitTest + children list
  3. UiPanel container that composites children
  4. UiRoot that implements IDevice semantics (focus, modal, capture, drag) and can be driven by GameWindow's existing Silk.NET input handlers
  5. UiSpriteBatch — derived from our existing TextRenderer but with scissor support added
  6. UiRenderer — per-frame entry point, walks the tree
  7. Integration stub in GameWindow.cs — creates UiRoot, routes Silk.NET mouse/keyboard to it with "world fall-through" for any unconsumed event

Later (follow-up sessions):

  • AcFont from portal.dat Font DBObj (slice 3 has exact on-disk layout)
  • CursorManager for dat-cursor support
  • First real panel — chat window — since we have wire messages ready
  • Vital orbs once server sends GameMessagePrivateUpdateVital

7.3 Event type mapping to Silk.NET input

Silk.NET event UiEvent type
IMouse.MouseDown (Left) 0x201
IMouse.MouseUp (Left) 0x202
IMouse.MouseMove 0x200
IMouse.Scroll 0x0A
IKeyboard.KeyDown 0x100
IKeyboard.KeyUp 0x101
IKeyboard.KeyChar 0x102
(synthesized on hover-delay) 0x07 (tooltip)
(synthesized on drag distance) 0x15/0x1C/0x21/0x3E drag-chain
(synthesized by UiRoot) 0x05/0x06 hover enter/leave

7.4 Key constants to preserve

Using retail's magic numbers lets hand-ported panel code (e.g., a future port of FUN_0047ba70AttributesPanel) use the same event_type switches the original uses:

public static class UiEventType
{
    public const int Click          = 0x01;
    public const int HoverEnter     = 0x05;
    public const int HoverLeave     = 0x06;
    public const int Tooltip        = 0x07;
    public const int DoubleClick    = 0x08;
    public const int Scroll         = 0x0A;
    public const int RightClick     = 0x0E;
    public const int DragBegin      = 0x15;
    public const int DragOver       = 0x1C;
    public const int DragEnter      = 0x21;
    public const int DropReleased   = 0x3E;
    public const int MouseDown      = 0x201;
    public const int MouseUp        = 0x202;
    public const int MouseMove      = 0x200;
    public const int KeyDown        = 0x100;
    public const int KeyUp          = 0x101;
    public const int Char           = 0x102;
}

7.5 Integration into existing GameWindow.cs

Current state: GameWindow binds Silk.NET IInputContext directly to PlayerMovementController + camera. We insert UiRoot between:

// In OnLoad:
_uiRoot = new UiRoot();
_uiRoot.WorldMouseFallThrough += (button, x, y, flags) => { /* existing camera/world handling */ };
_uiRoot.WorldKeyFallThrough  += (vk, lp) => { /* existing camera/world hotkeys */ };

mouse.MouseDown += (_, b) => _uiRoot.OnMouseDown(MapButton(b), (int)m.Position.X, (int)m.Position.Y, 0);
// similar for MouseUp, MouseMove, Scroll, KeyDown, KeyUp, KeyChar

UiRoot dispatches to focused widget / modal / top panel first; if no widget consumes the event, the WorldMouseFallThrough / WorldKeyFallThrough events fire, preserving the existing camera controls (sensitivity, RMB orbit, etc.).

7.6 Render integration

In GameWindow.OnRender, after the 3D scene and before the debug HUD:

// 3D world (existing)
_terrain?.Draw(camera, frustum);
_staticMesh?.Draw(...);
_debugLines?.Flush(...);

// Retail UI pass
_uiRenderer.BeginFrame(screenSize);
_uiRoot.Draw(_uiRenderer);
_uiRenderer.EndFrame();   // flushes sprite batches

// Existing debug overlay (dev HUD)
_debugOverlay?.Draw(...);

8. What we do NOT port

8.1 Keystone itself

keystone.dll is binary-only and not decompiled. We do not attempt to match its internal API. Our widget toolkit is analogous but not byte- compatible.

8.2 XML layout loader

Retail stores panel layouts as MSXML4-parsed XML. Our panels are code-defined for the MVP. If we ever want to consume the retail .dat-embedded layouts directly, we write an XML->widget-tree deserializer in a later phase.

8.3 ACHelpPlugin's embedded browser

The help/tutorial pane is a hosted Internet Explorer control (CoCreateInstance(CLSID_007cc680)). We ship our own help pane (flat text or a WebView2 embed if we ever need HTML).

8.4 IME composition

The production build has the IME hook stubbed. We don't target CJK localizations for the MVP.


9. Risk + sequencing

9.1 What can ship next session (low risk)

  1. UiElement + UiPanel + UiRoot + UiSpriteBatch scaffold (this session)
  2. Port BitmapFontAcFont from portal.dat Font DBObj
  3. Chat window panel (wire messages already parsed by WorldSession)

9.2 Medium risk

  1. Vital orbs + radar (needs server PrivateUpdateVital wiring)
  2. Inventory panel (needs item CreateObject parsing + icon lookup)
  3. Drag-drop plumbing (requires tooltip timer + capture + payload state)

9.3 High risk

  1. Character creation screen (needs the whole Attribute/Skill model)
  2. Trade window (multi-player state, confirms)
  3. Spellcasting UI (component consumption, formula display, cast progress)

10. Lookup table: retail function → proposed C# method

Only the high-confidence mappings. Many more in the slice docs.

Retail address Role Proposed C# class.method
0x004013A0 WinMain Program.Main
0x0043BA60 CreateMainWindow GameWindow.OnLoad
0x00439E50 PumpMessages (Silk.NET internal)
0x00439860 WndProc (Silk.NET internal; our UiRoot handles)
0x0045D0B0 RenderFrame GameWindow.OnRender
0x004554B0 GameTick GameWindow.OnUpdate
0x0043FCD0 Flush+Present (Silk.NET internal)
0x005DA8F0 UI tree walker UiRenderer.Draw(UiRoot)
0x00697140 Font load+bake AcFont constructor
0x00697770 Text→quads UiSpriteBatch.DrawString
0x006974D0 Flush text UiSpriteBatch.Flush
0x0043DCD0 px→NDC UiSpriteBatch private helper
0x0043C1C0 HCURSOR from bitmap CursorManager.SetFromDat
0x00557850 KeystoneCreate call UiRoot constructor
0x00692470 Chat panel render ChatWindow.Draw
0x0047BA70 Attributes panel render AttributesPanel.Draw
0x004A5200 Paperdoll slot tooltip PaperdollPanel.BuildSlotTooltip

11. Summary — what this pass ships today

  • Full research (this synthesis + 6 detailed slice docs)
  • C# port scaffold (sections 7.1-7.6):
    • UiEvent, UiEventType, UiElement, UiPanel, UiRoot, UiSpriteBatch
    • Integration stub in GameWindow — hooks Silk.NET events through UiRoot with world fall-through
  • Updated architecture docdocs/architecture/acdream-architecture.md gets a new UI-layer section pointing at the synthesis
  • Roadmap — new phase U1 Retail UI Framework appended

Not shipped this session (scheduled for follow-up):

  • Font loading from portal.dat (AcFont from Font DBObj)
  • CursorManager with dat-sourced custom cursors
  • Any concrete panel (chat / attributes / etc.)
  • Vital orb HUD
  • Scissor clip support in UiSpriteBatch

12. Open questions

  1. Widget IDs source. The hardcoded 0x1000001c, 0x100002fc resource IDs — are these in keystone.dll's layout tables, or in 0x21xxxxxx LayoutDesc dats? Likely dats; confirm by dumping one LayoutDesc and looking for these IDs inside.
  2. Tooltip delay. Retail registers the timer with some constant ms; grep for 0x3e8 (1000) or 0x1f4 (500) near RegisterTimerEvent(7, …).
  3. Chat protocol gaps. We know 6 wire messages drive chat; need to verify tab-filter persistence (is the "General" tab's filter mask stored server-side or client-side?).
  4. Inventory weight warnings. The error string catalog mentioned in slice 05 needs full enumeration when we port the inventory panel.
  5. Quickbar persistence. Server-side or saved to .ini?