Landed the UI framework design in 2026-04-24-ui-framework.md yesterday;
this commit propagates the decisions across the documents that future
sessions touch first, so the three-layer pattern is discoverable without
re-reading the full plan.
Changes:
* NEW memory/project_ui_architecture.md — evergreen crib-sheet:
three-layer diagram, AcDream.UI.Abstractions contract, D.2a/D.2b
split, module layout, hard rules, why staged not pure-custom.
* CLAUDE.md: new paragraph describing the three-layer UI split, naming
AcDream.UI.Abstractions as the plugin-facing contract, pointing at
the full plan + memory crib.
* docs/architecture/acdream-architecture.md: new "UI Architecture"
companion-stack diagram after Layer 0-5 (doesn't renumber the main
stack), plus step 6a "UI tick" in Per-Frame Update Order.
* docs/plans/2026-04-11-roadmap.md Phase D tightened:
- D.2 split explicitly into D.2a (Hexa.NET.ImGui scaffold + abstraction
layer) and D.2b (custom retail-look backend, implements same contracts).
- D.3 AcFont / D.4 dat sprites / D.7 cursor flagged as D.2b dependencies.
- D.5 core panels / D.6 HUD flagged as abstraction-layer deliverables
— ship with D.2a, reskinned by D.2b.
- D.8 Sound marked superseded (shipped as Phase E.2).
- F.5 core panels + H.1 chat-window cross-references updated to say
they target AcDream.UI.Abstractions, unblocked by D.2a.
- Shipped-phases table untouched.
* docs/research/retail-ui/00-master-synthesis.md: scope note at top
clarifies the Keystone research is the D.2b (custom backend)
foundation, NOT where D.2a starts.
* ~/.claude/.../memory/MEMORY.md: one-line index entry pointing at the
new project_ui_architecture.md (so session auto-load surfaces it).
Zero code changes; doc-only. dotnet build stays green. All verification
greps pass (see plan file for exact checks).
31 KiB
Retail AC Client GUI — Master Synthesis
Scope note (2026-04-24): 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 (Hexa.NET.ImGui scaffold). When reading this for implementation guidance, assume D.2a has shipped a working
AcDream.UI.Abstractionslayer (IPanel,IPanelRenderer, ViewModels, Commands) and you are building the custom retained-mode toolkit that implements the same contracts using dat-sourced fonts / sprites / cursors. Seedocs/plans/2026-04-24-ui-framework.mdfor the staged UI strategy andmemory/project_ui_architecture.mdfor the one-page crib-sheet.
Date: 2026-04-17
Sources: 6 parallel Opus research passes over the decompiled
acclient.exe (22,225 functions / 688K lines). Individual deep dives:
01-architecture-and-init.md— process entry → window → device → frame loop02-class-hierarchy.md— UI class hierarchy + virtual dispatch03-rendering.md— font, sprite, text, cursor render pipeline04-input-events.md— Win32 WndProc → Device → widget event routing05-panels.md— chat, attributes, skills, spell, paperdoll, inventory, etc.06-hud-and-assets.md— globes, radar, compass + dat-file UI assets
This document is the combined, cross-referenced picture of how the retail AC client renders and drives its UI, plus the port plan for acdream.
0. TL;DR — single most important finding
The retail UI widget toolkit is not in acclient.exe. The client
loads keystone.dll (plus two plugins ACHelpPlugin.dll,
ACPluginManager.dll) at startup via LoadLibraryA, gets a single
COM-like interface pointer (DAT_00870c2c), and delegates all widget
layout, hit-testing, input, and drawing to Keystone. The client
supplies primitives Keystone consumes (CFont, CSurface, CString,
the D3D device) and routes commands by resource ID (0x10000000+
custom event IDs, 0x21xxxxxx LayoutDesc IDs, 0x06xxxxxx sprites,
0x40000xxx fonts).
Implication for acdream: we cannot "port a C++ widget hierarchy from the decompile" — there is none. We implement our own retained-mode toolkit on top of Silk.NET OpenGL, consuming the same dat-borne textures and fonts so the UI is visually identical to retail even though the class hierarchy underneath is new. This is the same approach AC2D took. Section 11 below describes the C# layout.
1. Process / frame architecture (from slice 01)
WinMain @ 0x004013A0 (chunk_00400000.c)
└─ App::Initialize (vtable[0x10])
├─ dat cache init, string-table init
├─ vtable[0x74] "CreateMainWindow"
│ ├─ RegisterClassA("Turbine Device Class")
│ ├─ CreateWindowExA(..) → DAT_008381a4 (HWND)
│ ├─ graphics factory → DAT_0086734c
│ ├─ render device → DAT_00837ff4 (lightweight)
│ ├─ Turbine Core → DAT_00870340 (GL surfaces + dats)
│ └─ Keystone bring-up:
│ LoadLibraryA("keystone.dll") → DAT_00870c30
│ LoadLibraryA("plugins\\ACHelpPlugin.dll")
│ LoadLibraryA("plugins\\ACPluginManager.dll")
│ GetProcAddress "KeystoneCreate" → DAT_00870c34
│ KeystoneCreate(hwnd, core, cwd, 0,0,0,0) → DAT_00870c2c
│
├─ App::Run (vtable[0x1c]) — main loop forever
│ while (!quit) {
│ PumpMessages @ 0x00439E50 // FUN_00439e50
│ PeekMessageA
│ IME filter (stub)
│ Keystone->TranslateAccelerator (vtable +0x6c)
│ TranslateMessage + DispatchMessageA → WndProc @ 0x00439860
│ RenderFrame @ 0x0045D0B0 // 3D world + UI + overlays
│ GameTick @ 0x004554B0 // physics + AI + scripts
│ FlushPresent @ 0x0043FCD0 // clear + begin + UI + end + Present
│ Sleep(~1ms)
│ }
└─ App::Shutdown (vtable[0x2c])
Frame body ordering (important): render → tick → Sleep. Retail renders BEFORE the game tick, because the render pass reads a stable snapshot built at the end of the previous frame (double-buffered simulation state). acdream already follows this pattern.
Global state block (acdream should replicate the 4-way factoring):
| Retail global | Role | Proposed C# class |
|---|---|---|
DAT_008381a4 |
HWND | (Silk.NET IWindow) |
DAT_0086734c |
Graphics factory | (handled by Silk.NET GL selection) |
DAT_00870340 |
Turbine Core (GL + dats + fonts) | AcDream.App.UI.UiHost |
DAT_00837ff4 |
Render/input device | AcDream.App.UI.UiDevice |
DAT_00870c2c |
Keystone root (widgets) | AcDream.App.UI.UiRoot (our toolkit) |
2. Rendering pipeline (from slice 03)
2.1 Top-level frame
FUN_0043fcd0 SceneTool::RenderFrame
├─ RenderDevice::Clear(color|depth)
├─ if world.visible:
│ Scene::BeginFrame
│ Scene::RenderWorld (terrain, statics, lights)
├─ if DAT_0083846c != 0: FUN_005da8f0() <-- UI PANEL TREE WALK (7.8 KB func)
├─ if DAT_00838468 != 0: FUN_00692470() <-- CHAT TEXT + EDIT LINE
├─ FUN_0043f7f0() <-- DEBUG/STATS HUD
├─ RenderDevice::Clear(color only)
├─ RenderDevice::BeginScene
├─ RenderDevice::EndScene + Present
└─ FUN_0043e6b0() <-- frame stats
2.2 Single sprite / text quad
- Vertex format: 24 bytes =
(x, y, z=0, rhw, u, v)— XYZRHW + TEX1 - No per-vertex color; color is set via
D3DRS_TEXTUREFACTORper draw - 6 verts per quad (TL, BL, BR, BR, TR, TL — CCW with D3D conventions)
- NDC conversion applies a DX9-style half-pixel offset
- Global buffer at
DAT_008f9a90..8f9a98(base, capacity, write-offset) - Hot loop unrolled 4 glyphs per iteration
- Flush:
FUN_006974d0→FUN_005a26d0(D3DPT_TRIANGLELIST, verts/3, pV, 0x142, tex, tex, stride=24)
2.3 Fonts
- DBObj type
Fontat IDs0x40000000..0x40000FFF(portal.dat) - On-disk
FontCharDescper 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/ChannelBroadcastmessages
2.4 UI textures
| ID range | DBObjType | Role |
|---|---|---|
0x06000000..0x06FFFFFF |
RenderSurface | All button images, panel backgrounds, icons, cursor bitmaps, font glyph sheets |
0x08000000..0x08FFFFFF |
SurfaceTexture | Wraps a RenderSurface with palette/tiling info |
0x0F000000..0x0FFFFFFF |
SurfaceMaterial | Materials referencing SurfaceTextures |
0x40000000..0x40000FFF |
Font | Font metadata — references a RenderSurface |
0x21xxxxxx |
LayoutDesc | Panel layouts — tree of ElementDescs + StateDescs |
0x23/0x24xxxxxx |
StringTable | Localized UI strings |
0x14xxxxxx |
MasterInputMap | Default keybinds |
0x41xxxxxx |
LanguageInfo | Localization root |
2.5 Blend + depth + clipping
- Blend:
SRC_ALPHA / INV_SRC_ALPHA("over") - Depth: off for UI
- Clipping: retail mostly pre-clips at the vertex-generation stage; true hardware scissor is used in some panel interiors (chat scroll, inventory grid)
3. Class hierarchy / polymorphism (from slice 02)
3.1 What is in acclient.exe
| Class | Vtable | Role |
|---|---|---|
CUIManager (singleton) |
PTR_FUN_00799fc4 |
DAT_00838374; AddListener/RemoveListener at vtable +0x0c |
CUIListener (embedded base) |
PTR_FUN_00801670 |
Any object wanting UI events embeds this as MI secondary base |
CFont (chunk_00440000.c) |
— | Glyph lookup, range map, atlas blit |
CSurface |
PTR_FUN_0079c26c |
40-byte bitmap wrapper (w, h, bpp, pixels) |
CString helpers |
FUN_00402490/07e40/0b8f0 |
Refcounted wide-string buffer (string builder) |
3.2 The Keystone interface (DAT_00870c2c)
Observed vtable slots (from call sites in acclient.exe):
| Slot | Role |
|---|---|
| +0x08 | Release / destructor |
| +0x14 | FindPlugin(wchar_t* name) |
| +0x20 | ProcessFrame — called once per frame from FUN_0043fcd0 |
| +0x24 | CreateOrActivate(a, b, c, d) — 4-arg plugin entry |
| +0x28 | ClearActive(0) |
| +0x2c | GetActive |
| +0x5c | SendCommand(id, op, buffer) — generic command dispatch |
| +0x60 | HitTest(POINT*) |
| +0x6c | TranslateAccelerator(hwnd, haccel, &MSG) — pump's pre-filter |
3.3 Polymorphism pattern
Classic MSVC multiple inheritance with per-base-class vtables. A
widget like CharGen has four vtables at fixed offsets inside the
object (+0x00, +0x04, +0x08, +0x12C). AddListener is passed
the INNER this pointer (the listener subobject), not the outer.
3.4 Port decision
acdream implements its own retained-mode widget toolkit. Keystone
is not decompiled here and reverse-engineering it is out of scope. We
build UiElement / UiPanel / UiButton / UiLabel / UiEditBox /
etc. with virtual OnDraw / OnHitTest / OnMouseMessage /
OnKeyMessage — not multiple inheritance.
4. Input / event routing (from slice 04)
4.1 Message pump
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
(
0x200mouse-move,0x201LDown,0x202LUp,0x204/205RDown/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 tokensTooltipBuilder—FUN_0042dc80open → content →FUN_0042e590closeDragDropRouter— routes 0x15 / 0x1C / 0x21 / 0x3E chainsRowList<T>— used by skills, spells, fellowship members, allegiance treePropertyUpdateBus— dispatchesGameMessagePrivateUpdate*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 attributeGameMessagePrivateUpdateVital(0x02E7) — 25 bytesGameMessagePrivateUpdateAttribute2ndLevel(0x02E9) — max-value refreshGameEventPlayerDescription(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) →HCURSORviaCreateIconIndirect - Rendering mode: OS cursor suppressed when UI draws its own sprite
SetCursorPosused 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:
UiEventstruct + event type enum (24 bytes, matches retail)UiElementabstract base with virtual Draw/OnMouse/OnKey/OnTick/OnHitTest + children listUiPanelcontainer that composites childrenUiRootthat implementsIDevicesemantics (focus, modal, capture, drag) and can be driven byGameWindow's existing Silk.NET input handlersUiSpriteBatch— derived from our existingTextRendererbut with scissor support addedUiRenderer— per-frame entry point, walks the tree- Integration stub in
GameWindow.cs— createsUiRoot, routes Silk.NET mouse/keyboard to it with "world fall-through" for any unconsumed event
Later (follow-up sessions):
AcFontfrom portal.dat Font DBObj (slice 3 has exact on-disk layout)CursorManagerfor dat-cursor support- First real panel — chat window — since we have wire messages ready
- Vital orbs once server sends
GameMessagePrivateUpdateVital
7.3 Event type mapping to Silk.NET input
| Silk.NET event | UiEvent type |
|---|---|
| IMouse.MouseDown (Left) | 0x201 |
| IMouse.MouseUp (Left) | 0x202 |
| IMouse.MouseMove | 0x200 |
| IMouse.Scroll | 0x0A |
| IKeyboard.KeyDown | 0x100 |
| IKeyboard.KeyUp | 0x101 |
| IKeyboard.KeyChar | 0x102 |
| (synthesized on hover-delay) | 0x07 (tooltip) |
| (synthesized on drag distance) | 0x15/0x1C/0x21/0x3E drag-chain |
| (synthesized by UiRoot) | 0x05/0x06 hover enter/leave |
7.4 Key constants to preserve
Using retail's magic numbers lets hand-ported panel code (e.g., a
future port of FUN_0047ba70 → AttributesPanel) use the same
event_type switches the original uses:
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)
UiElement+UiPanel+UiRoot+UiSpriteBatchscaffold (this session)- Port
BitmapFont→AcFontfrom portal.datFontDBObj - Chat window panel (wire messages already parsed by
WorldSession)
9.2 Medium risk
- Vital orbs + radar (needs server
PrivateUpdateVitalwiring) - Inventory panel (needs item CreateObject parsing + icon lookup)
- Drag-drop plumbing (requires tooltip timer + capture + payload state)
9.3 High risk
- Character creation screen (needs the whole Attribute/Skill model)
- Trade window (multi-player state, confirms)
- Spellcasting UI (component consumption, formula display, cast progress)
10. Lookup table: retail function → proposed C# method
Only the high-confidence mappings. Many more in the slice docs.
| Retail address | Role | Proposed C# class.method |
|---|---|---|
0x004013A0 |
WinMain | Program.Main |
0x0043BA60 |
CreateMainWindow | GameWindow.OnLoad |
0x00439E50 |
PumpMessages | (Silk.NET internal) |
0x00439860 |
WndProc | (Silk.NET internal; our UiRoot handles) |
0x0045D0B0 |
RenderFrame | GameWindow.OnRender |
0x004554B0 |
GameTick | GameWindow.OnUpdate |
0x0043FCD0 |
Flush+Present | (Silk.NET internal) |
0x005DA8F0 |
UI tree walker | UiRenderer.Draw(UiRoot) |
0x00697140 |
Font load+bake | AcFont constructor |
0x00697770 |
Text→quads | UiSpriteBatch.DrawString |
0x006974D0 |
Flush text | UiSpriteBatch.Flush |
0x0043DCD0 |
px→NDC | UiSpriteBatch private helper |
0x0043C1C0 |
HCURSOR from bitmap | CursorManager.SetFromDat |
0x00557850 |
KeystoneCreate call | UiRoot constructor |
0x00692470 |
Chat panel render | ChatWindow.Draw |
0x0047BA70 |
Attributes panel render | AttributesPanel.Draw |
0x004A5200 |
Paperdoll slot tooltip | PaperdollPanel.BuildSlotTooltip |
11. Summary — what this pass ships today
- Full research (this synthesis + 6 detailed slice docs)
- C# port scaffold (sections 7.1-7.6):
UiEvent,UiEventType,UiElement,UiPanel,UiRoot,UiSpriteBatch- Integration stub in
GameWindow— hooks Silk.NET events through UiRoot with world fall-through
- Updated architecture doc —
docs/architecture/acdream-architecture.mdgets 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
- Widget IDs source. The hardcoded
0x1000001c,0x100002fcresource IDs — are these inkeystone.dll's layout tables, or in0x21xxxxxxLayoutDesc dats? Likely dats; confirm by dumping one LayoutDesc and looking for these IDs inside. - Tooltip delay. Retail registers the timer with some constant ms;
grep for
0x3e8(1000) or0x1f4(500) nearRegisterTimerEvent(7, …). - 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?).
- Inventory weight warnings. The error string catalog mentioned in slice 05 needs full enumeration when we port the inventory panel.
- Quickbar persistence. Server-side or saved to
.ini?