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.
93 lines
3.1 KiB
C#
93 lines
3.1 KiB
C#
using System.Numerics;
|
|
|
|
namespace AcDream.App.UI;
|
|
|
|
/// <summary>
|
|
/// Rectangular container with an optional translucent background and
|
|
/// border. Used as the base of every retail panel (attributes, chat,
|
|
/// inventory, login, etc.).
|
|
///
|
|
/// Retail has panel background art stored as 9-slice sprite assets in
|
|
/// the <c>0x06xxxxxx</c> RenderSurface range, and composed via
|
|
/// <c>LayoutDesc</c> (<c>0x21xxxxxx</c>) trees. Until our
|
|
/// <c>AcFont</c>/<c>UiSpriteBatch</c> consumes those directly, we draw a
|
|
/// simple translucent rectangle so panels are visible during development.
|
|
/// </summary>
|
|
public class UiPanel : UiElement
|
|
{
|
|
/// <summary>Background fill color. Set <see cref="Vector4.Zero"/> to skip.</summary>
|
|
public Vector4 BackgroundColor { get; set; } = new(0f, 0f, 0f, 0.55f);
|
|
|
|
/// <summary>Border color. Set <see cref="Vector4.Zero"/> to skip.</summary>
|
|
public Vector4 BorderColor { get; set; } = new(0.15f, 0.15f, 0.2f, 0.8f);
|
|
|
|
public float BorderThickness { get; set; } = 1f;
|
|
|
|
protected override void OnDraw(UiRenderContext ctx)
|
|
{
|
|
if (BackgroundColor.W > 0f)
|
|
ctx.DrawRect(0, 0, Width, Height, BackgroundColor);
|
|
|
|
if (BorderColor.W > 0f && BorderThickness > 0f)
|
|
ctx.DrawRectOutline(0, 0, Width, Height, BorderColor, BorderThickness);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Static text label. Draws a single line of text using the context's
|
|
/// default font (or an override). Does not consume input.
|
|
///
|
|
/// Equivalent retail primitive: wide-string appended to a CString via
|
|
/// <c>FUN_0040b8f0</c> then drawn by the widget's draw method through
|
|
/// <c>FUN_00698330</c>.
|
|
/// </summary>
|
|
public class UiLabel : UiElement
|
|
{
|
|
public string Text { get; set; } = string.Empty;
|
|
public Vector4 TextColor { get; set; } = new(1f, 1f, 1f, 1f);
|
|
|
|
public UiLabel() { ClickThrough = true; }
|
|
|
|
protected override void OnDraw(UiRenderContext ctx)
|
|
=> ctx.DrawString(Text, 0, 0, TextColor);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Simple clickable button: panel background + centered label + click
|
|
/// callback. Retail equivalent is Keystone's button widget, driven by
|
|
/// a <c>StateDesc</c> per <c>UIStateId</c> (normal / hot / pressed /
|
|
/// disabled) from the panel layout.
|
|
/// </summary>
|
|
public class UiButton : UiPanel
|
|
{
|
|
public string Text { get; set; } = string.Empty;
|
|
public Vector4 TextColor { get; set; } = new(1f, 1f, 1f, 1f);
|
|
public event System.Action? Click;
|
|
|
|
public UiButton()
|
|
{
|
|
BackgroundColor = new Vector4(0.1f, 0.1f, 0.15f, 0.8f);
|
|
BorderColor = new Vector4(0.45f, 0.45f, 0.55f, 1f);
|
|
}
|
|
|
|
public override bool OnEvent(in UiEvent e)
|
|
{
|
|
if (e.Type == UiEventType.Click && Enabled)
|
|
{
|
|
Click?.Invoke();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected override void OnDraw(UiRenderContext ctx)
|
|
{
|
|
base.OnDraw(ctx);
|
|
if (Text.Length == 0 || ctx.DefaultFont is null) return;
|
|
|
|
float textW = ctx.DefaultFont.MeasureWidth(Text);
|
|
float tx = (Width - textW) * 0.5f;
|
|
float ty = (Height - ctx.DefaultFont.LineHeight) * 0.5f;
|
|
ctx.DrawString(Text, tx, ty, TextColor);
|
|
}
|
|
}
|