Deep investigation of the retail AC client's GUI subsystem, driven by 6
parallel Opus research agents, plus the first cut of a retail-faithful
retained-mode widget toolkit that scaffolds Phase D.
Research (docs/research/retail-ui/):
- 00-master-synthesis.md — cross-slice synthesis + port plan
- 01-architecture-and-init.md — WinMain, CreateMainWindow, frame loop,
Keystone bring-up (7 globals mapped)
- 02-class-hierarchy.md — key finding: UI lives in keystone.dll,
not acclient.exe; CUIManager + CUIListener
MI pattern, CFont + CSurface + CString
- 03-rendering.md — 24-byte XYZRHW+UV verts, per-font
256x256 atlas baked from RenderSurface,
TEXTUREFACTOR coloring, DrawPrimitiveUP
- 04-input-events.md — Win32 WndProc → Device (DAT_00837ff4)
→ widget OnEvent(+0x128); full event-type
table (0x01 click, 0x07 tooltip ~1000ms,
0x15 drag-begin, 0x21 enter, 0x3E drop)
- 05-panels.md — chat, attributes, skills, spells, paperdoll
(25-slot layout), inventory, fellowship,
allegiance — with wire-message bindings
- 06-hud-and-assets.md — vital orbs (scissor fill), radar
(0x06001388/0x06004CC1, 1.18× shrink),
compass strip, dat asset catalog
Key insight: keystone.dll owns the actual widget toolkit — we cannot
port a class hierarchy from the decompile because it's not there.
Instead we implement our own retained-mode toolkit with retail-faithful
behavior (event codes, focus/modal/capture, drag-drop state machine)
and will consume the same portal.dat fonts + sprites so the visual
identity is preserved.
C# scaffold (src/AcDream.App/UI/):
- UiEvent — 24-byte event struct + retail event-type constants
(0x01 click, 0x15 drag-begin, 0x201 WM_LBUTTONDOWN,
etc.) matching retail decompile switches
- UiElement — base widget: children, ZOrder, focus/capture flags,
virtual OnDraw/OnEvent/OnHitTest/OnTick; children-
first hit test + back-to-front composite
- UiPanel — panel, label, button primitives
- UiRenderContext — 2D draw context with translate stack
- UiRoot — top-of-tree + Device responsibilities (mouse/
keyboard state, focus, modal, capture, drag-drop,
tooltip timer); WorldMouseFallThrough/
WorldKeyFallThrough preserves existing camera
controls when no widget consumes
- UiHost — packages UiRoot + TextRenderer + input wiring
helpers for one-line integration into GameWindow
- README.md — orientation for future agents
Roadmap (docs/plans/2026-04-11-roadmap.md):
- D.1 marked shipped (debug overlay from 2026-04-17)
- D.2 expanded to include the retail UI framework landed here
- D.3-D.7 added: AcFont, dat sprites, core panels, HUD, CursorManager
- D.8 remains sound
All existing 470 tests pass. 0 warnings, 0 errors.
90 lines
3.3 KiB
C#
90 lines
3.3 KiB
C#
using System.Numerics;
|
|
|
|
namespace AcDream.App.UI;
|
|
|
|
/// <summary>
|
|
/// Per-event payload delivered to <see cref="UiElement.OnEvent"/>.
|
|
/// Mirrors the retail AC client's 24-byte event struct that is passed to
|
|
/// every widget's vtable slot +0x128 (<c>OnEvent(int* event)</c>).
|
|
///
|
|
/// Layout from decompiled <c>chunk_004A0000.c</c> paperdoll handler
|
|
/// <c>FUN_004A5FA0</c>:
|
|
/// <code>
|
|
/// int source_id; // param_2[0] — e.g. 0x100001d6 (drag source)
|
|
/// void* target_widget; // param_2[1]
|
|
/// int event_type; // param_2[2] — see UiEventType
|
|
/// int data0; // param_2[3]
|
|
/// int data1; // param_2[4] — typically x in local coords
|
|
/// int data2; // param_2[5] — typically y
|
|
/// int data3; // param_2[6]
|
|
/// </code>
|
|
/// </summary>
|
|
public readonly record struct UiEvent(
|
|
uint SourceId,
|
|
UiElement? Target,
|
|
int Type, // see <see cref="UiEventType"/>
|
|
int Data0 = 0,
|
|
int Data1 = 0,
|
|
int Data2 = 0,
|
|
int Data3 = 0,
|
|
object? Payload = null);
|
|
|
|
/// <summary>
|
|
/// Retail AC UI event-type constants. Each value matches the decompiled
|
|
/// switch-case in widgets' OnEvent handlers (e.g. 0x01 click, 0x15 drag
|
|
/// begin, 0x3E drop released). Win32 WM_* numbers are reused for raw
|
|
/// button/key/mouse events (0x200 = WM_MOUSEMOVE etc.) — this matches
|
|
/// retail where internal event codes collide deliberately with WM_*.
|
|
///
|
|
/// Evidence from decompile:
|
|
/// - 0x01 click — chunk_00470000.c ~11140, chunk_004C0000.c ~9270
|
|
/// - 0x05/0x06 hover — chunk_00460000.c ~6253
|
|
/// - 0x07 tooltip — chunk_00460000.c ~6253 (delayed via
|
|
/// Device::RegisterTimerEvent(7, widget, delayMs))
|
|
/// - 0x0A scroll — chunk_00470000.c ~11210
|
|
/// - 0x0E right-click— chunk_004A0000.c ~2674
|
|
/// - 0x15 drag begin — chunk_004A0000.c ~2707
|
|
/// - 0x1C drag-over — chunk_004A0000.c ~2723
|
|
/// - 0x21 drag-enter — chunk_004A0000.c ~2714
|
|
/// - 0x3E drop-released — chunk_004A0000.c ~2754
|
|
/// </summary>
|
|
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 FocusLost = 0x28;
|
|
public const int FocusGained = 0x29;
|
|
public const int DropReleased = 0x3E;
|
|
|
|
// Raw Win32-style event numbers (retail uses WM_* verbatim for internal dispatch).
|
|
public const int MouseMove = 0x200;
|
|
public const int MouseDown = 0x201; // left button down
|
|
public const int MouseUp = 0x202; // left button up
|
|
public const int DoubleClickLeft = 0x203;
|
|
public const int RightDown = 0x204;
|
|
public const int RightUp = 0x205;
|
|
public const int MiddleDown = 0x207;
|
|
public const int MiddleUp = 0x208;
|
|
|
|
public const int KeyDown = 0x100;
|
|
public const int KeyUp = 0x101;
|
|
public const int Char = 0x102;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mouse button enum matching retail's 1/2/3 encoding.
|
|
/// </summary>
|
|
public enum UiMouseButton
|
|
{
|
|
Left = 1,
|
|
Right = 2,
|
|
Middle = 3,
|
|
}
|