docs+feat(ui): retail UI deep-dive research + C# port scaffold
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.
This commit is contained in:
parent
ff325abd7b
commit
7230c1590f
15 changed files with 8041 additions and 5 deletions
111
src/AcDream.App/UI/README.md
Normal file
111
src/AcDream.App/UI/README.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# AcDream.App.UI — Retail-style UI toolkit
|
||||
|
||||
This is acdream's retained-mode UI toolkit. It mirrors the **behavior**
|
||||
of the retail AC client (hit-testing, modal, capture, drag-drop,
|
||||
tooltip delay, focus routing, event type codes) without trying to
|
||||
byte-match the retail binary — because the retail widgets live in
|
||||
`keystone.dll`, which we don't decompile.
|
||||
|
||||
## Research
|
||||
|
||||
All design decisions in this directory are grounded in the master
|
||||
synthesis + six deep-dive docs under
|
||||
[`docs/research/retail-ui/`](../../../docs/research/retail-ui/):
|
||||
|
||||
| Document | Topic |
|
||||
|---|---|
|
||||
| [`00-master-synthesis.md`](../../../docs/research/retail-ui/00-master-synthesis.md) | Cross-slice synthesis + C# port plan |
|
||||
| [`01-architecture-and-init.md`](../../../docs/research/retail-ui/01-architecture-and-init.md) | Process entry, window, main loop |
|
||||
| [`02-class-hierarchy.md`](../../../docs/research/retail-ui/02-class-hierarchy.md) | CUIManager / CUIListener / CFont / CSurface |
|
||||
| [`03-rendering.md`](../../../docs/research/retail-ui/03-rendering.md) | Font atlas, 2D quad batch, cursor |
|
||||
| [`04-input-events.md`](../../../docs/research/retail-ui/04-input-events.md) | WndProc → Device → widget event routing |
|
||||
| [`05-panels.md`](../../../docs/research/retail-ui/05-panels.md) | Chat, attributes, spells, paperdoll, inventory |
|
||||
| [`06-hud-and-assets.md`](../../../docs/research/retail-ui/06-hud-and-assets.md) | Vital orbs, radar, compass + dat asset catalog |
|
||||
|
||||
## Files
|
||||
|
||||
- `UiEvent.cs` — 24-byte event struct + retail-faithful type constants
|
||||
(`0x01` click, `0x15` drag-begin, `0x3E` drop, `0x201` WM_LBUTTONDOWN, …)
|
||||
- `UiElement.cs` — base widget with `OnDraw` / `OnEvent` / `OnHitTest` /
|
||||
`OnTick` virtuals, children list, ZOrder, focus/capture flags
|
||||
- `UiPanel.cs` — `UiPanel` (rect + optional bg/border), `UiLabel`, `UiButton`
|
||||
- `UiRenderContext.cs` — per-frame draw context with translate stack
|
||||
- `UiRoot.cs` — top-of-tree + "Device" responsibilities (mouse/keyboard
|
||||
state, focus, modal, capture, drag-drop, tooltip timer). Mirrors the
|
||||
retail `DAT_00837ff4` Device object's vtable.
|
||||
- `UiHost.cs` — one-shot wrapper: owns the `UiRoot`, a `TextRenderer`,
|
||||
and a default `BitmapFont`. Provides `WireMouse` / `WireKeyboard`
|
||||
helpers for Silk.NET plumbing.
|
||||
|
||||
## Integration pattern
|
||||
|
||||
```csharp
|
||||
// GameWindow.OnLoad
|
||||
_uiHost = new UiHost(_gl!, shadersDir, _debugFont);
|
||||
_uiHost.Root.WorldMouseFallThrough += (btn, x, y, flags) => HandleWorldClick(btn, x, y, flags);
|
||||
_uiHost.Root.WorldKeyFallThrough += (vk, lp) => HandleHotkey(vk);
|
||||
foreach (var mouse in _input.Mice) _uiHost.WireMouse(mouse);
|
||||
foreach (var kb in _input.Keyboards) _uiHost.WireKeyboard(kb);
|
||||
|
||||
// Add panels
|
||||
var chat = new Panels.ChatWindow { Left = 10, Top = 400, Width = 500, Height = 250 };
|
||||
_uiHost.Root.AddChild(chat);
|
||||
|
||||
// GameWindow.OnRender — after the 3D scene
|
||||
_uiHost.Tick(deltaSeconds);
|
||||
_uiHost.Draw(new Vector2(_window!.Size.X, _window.Size.Y));
|
||||
```
|
||||
|
||||
## What's scaffolded vs what still needs building
|
||||
|
||||
### Shipped in the scaffold (this session)
|
||||
|
||||
- UI tree + event routing + focus + modal + capture + drag-drop
|
||||
- Hit-testing (children-first, Z-order tie-break)
|
||||
- Tooltip timer (~1000ms)
|
||||
- Hover enter/leave, click vs right-click, scroll, keyboard
|
||||
- World fall-through so existing camera/player controls still work
|
||||
- Simple text/rect drawing through the existing `BitmapFont` +
|
||||
`TextRenderer` pipeline
|
||||
|
||||
### To build next
|
||||
|
||||
1. **`AcFont`** + **`FontCache`** — load `Font` DBObjs from
|
||||
`portal.dat` range `0x40000000..0x40000FFF`, bake 256×256 glyph
|
||||
atlas from the referenced `RenderSurface` (`0x06xxxxxx`). See
|
||||
[slice 03 §4](../../../docs/research/retail-ui/03-rendering.md#4-fonts-in-the-dat-files).
|
||||
2. **Dat sprite loader** — decode `RenderSurface` dats as GL textures;
|
||||
add `DrawSprite(uint datId, Rectangle dest, uint rgba)` to
|
||||
`UiRenderContext`.
|
||||
3. **`CursorManager`** — OS cursor + dat-sourced custom cursors via
|
||||
[slice 03 §7](../../../docs/research/retail-ui/03-rendering.md#7-cursor).
|
||||
4. **Scissor clipping** — for panels with scrollable interiors (chat,
|
||||
inventory grid). `GL_SCISSOR_TEST` wrapped in
|
||||
`UiRenderContext.PushScissor` / `PopScissor`.
|
||||
5. **First concrete panel — `ChatWindow`** since we have all 6 wire
|
||||
messages parsed already. See
|
||||
[slice 05 §1](../../../docs/research/retail-ui/05-panels.md#1-chat-window).
|
||||
6. **Vital orbs HUD** once the server sends
|
||||
`GameMessagePrivateUpdateVital`. See
|
||||
[slice 06 A.1](../../../docs/research/retail-ui/06-hud-and-assets.md#a1-health--stamina--mana-globes).
|
||||
|
||||
## Retail magic numbers the scaffold preserves
|
||||
|
||||
Because hand-ported panel code will copy the retail switch-case
|
||||
structure, we keep the magic constants:
|
||||
|
||||
```csharp
|
||||
// Event types
|
||||
UiEventType.Click == 0x01 // chunk_00470000.c ~11140
|
||||
UiEventType.Tooltip == 0x07 // chunk_00460000.c ~6253 (~1000ms delay)
|
||||
UiEventType.DragBegin == 0x15 // chunk_004A0000.c ~2707
|
||||
UiEventType.DragEnter == 0x21 // chunk_004A0000.c ~2714
|
||||
UiEventType.DragOver == 0x1C // chunk_004A0000.c ~2723
|
||||
UiEventType.DropReleased == 0x3E // chunk_004A0000.c ~2754
|
||||
UiEventType.MouseDown == 0x201 // WM_LBUTTONDOWN
|
||||
UiEventType.MouseUp == 0x202 // WM_LBUTTONUP
|
||||
|
||||
// Event IDs
|
||||
// Widget event IDs live in the 0x10000000+ range (retail convention).
|
||||
// UiRoot auto-assigns EventIds starting at 0x10000001.
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue