acdream/docs/superpowers/specs/2026-06-15-chat-window-redrive-design.md
Erik 26cb34f126 @
docs(D.2b): chat-window re-drive design spec + list-ui-layouts research tool

Plan-2 chat piece of the LayoutDesc importer. Identifies the chat window as
LayoutDesc 0x21000006 (gmMainChatUI, element class 0x10000041) and grounds a
faithful, data-driven re-drive in the named retail decomp (ChatInterface +
gmMainChatUI + UIElement_Text/_Scrollable/_Scrollbar/_Menu) plus a user-provided
retail screenshot.

Design (full-faithful scope, user-approved):
- transcript = UIElement_Text 0x10000011 (dat font, bottom-pinned, 10k behead cap,
  pixel scroll, 1 line/wheel-notch)
- scrollbar = right-side track 0x10000012 + thumb 0x1000048c + up/down
- input = editable UIElement_Text 0x10000016 (caret, 100-entry history, Enter/Send)
- channel menu = UIElement_Menu 0x10000014 ("Chat" selector -> active channel)
- shared ChatCommandRouter extracted from ChatPanel
- screenshot correction: the four 0x10000522-525 left-edge elements are the
  numbered CHAT TABS (1-4), not scroll buttons (a research-agent inference the
  retail screenshot refutes)
- deferred (need non-UI plumbing, each gets a divergence row): tab switching/
  filtering, squelch, clickable name-tags, in-element word-wrap, styled runs,
  font config, opacity transition

Tooling: AcDream.Cli `list-ui-layouts <datdir> [0xRootType]` — read-only index of
every UI LayoutDesc by root element class + size + element-Type histogram; how the
chat layout was located (root type 0x10000041). Reusable for future panel re-drives.

Spec: docs/superpowers/specs/2026-06-15-chat-window-redrive-design.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-15 19:38:27 +02:00

17 KiB
Raw Blame History

D.2b — Chat-window re-drive (LayoutDesc importer, Plan 2 chat piece) — design

Date: 2026-06-15 Branch: claude/hopeful-maxwell-214a12 (D.2b retail-UI track; lighting/M1.5 is a separate branch off main) Status: design — approved scope, pending spec review Predecessor: the LayoutDesc importer + the vitals re-drive (docs/superpowers/specs/2026-06-15-layoutdesc-importer-design.md, docs/research/2026-06-15-layoutdesc-format.md, claude-memory/project_d2b_retail_ui.md). Handoff that opened this work: docs/research/2026-06-15-chat-window-redrive-handoff.md.


1. Goal

Replace the hand-authored retail chat window (a UiNineSlicePanel hosting a UiChatView at a guessed rect, built inline in GameWindow.cs under if (_options.RetailUi)) with the data-driven retail chat window read from the dat LayoutDesc 0x21000006 (gmMainChatUI) via the existing LayoutImporter, with faithful behavioral widgets ported from the named retail decomp and the dat font — the same way the vitals window became data-driven.

The code is modern. The behavior is retail. Every widget algorithm is ported from docs/research/named-retail/acclient_2013_pseudo_c.txt with a cited class::method @address.

2. Approved scope

In scope (faithful core):

  • Data-driven window frame from 0x21000006 (bg sprites, resize bar, grip chrome, translucency).
  • Transcript: dat-font UIElement_Text port — scrollable, bottom-pinned, per-line chat-kind color, 10k-glyph behead cap.
  • Scrollbar: right-side track + thumb + up/down buttons, pixel-based scroll, thumbRatio = view/content, wheel = 1 line per notch.
  • Input: editable one-line field — caret, insert/delete, 100-entry command history (up/down arrow), focus sprite, Enter→submit.
  • Channel menu (Chat ▸): dropdown of channels; selection sets the active outbound channel (the ChatInputParser default channel).
  • Send button + max/min button.
  • ChatCommandRouter: the shared submit pipeline, extracted from ChatPanel so the ImGui devtools chat and the retail chat share one routing path.

Deferred (each gets a retail-divergence-register.md row — these need non-UI plumbing acdream lacks, they are NOT UI scope cuts):

  • Numbered chat tabs (14) — switching + per-tab chat-type filtering. The tab sprites render (they come free from the importer), but clicking a tab to filter which chat kinds show needs the per-tab m_llTextTypeFilter / m_chatNewNonVisibleTextIndicator system.
  • Squelch toggle (menu item 0) — needs a squelch subsystem.
  • Clickable name-tags (StartTell on click) — needs StringInfo/TextTag styled runs in ChatLog.
  • In-element word-wrap at panel width — the transcript renders pre-split ChatLog lines 1:1; faithful GlyphList::Recalculate wrap reworks the selection/hit-test model (visual-row ≠ record). Highest-risk UI piece; deferred.
  • Per-glyph mixed-color runs / configurable font face+size (gmClient::sm_nFontFace).
  • Active/inactive opacity switch — a single default translucency is in scope; the focused-brighter / unfocused-dimmer transition is deferred.

3. Retail reference (the port target)

gmMainChatUI (registered element class 0x10000041, built from LayoutDesc 0x21000006) extends a base ChatInterface. ChatInterface owns the transcript, input, inbound routing, submit, history, truncate and opacity; gmMainChatUI adds the channel menu, squelch, max/min, tab visibility and clickable name-tags.

3.1 Element → role map (0x21000006)

Element Type Role Decomp anchor
0x1000000E 0x10000041 window root (gmMainChatUI), bg 0x0600114D, 800×100 authored gmMainChatUI::Register @0x4cd350
0x1000000F 9 Resizebar top resize bar, img 0x06001125, cursor 0x06005E66
0x1000046F 0 max/min button (Maximized 0x06005E64 / Minimized 0x06005E65) gmMainChatUI::HandleMaximizeButton @0x4cce50
0x10000010 3 Field transcript panel bg 0x06001115
0x10000011 0 (UIElement_Text) transcript — read-only, multiline, scrollable ChatInterface::PostInit @0x4f3e47
0x1000048c 0 scroll thumb (child of transcript) ChatInterface::PostInit @0x4f3e79
0x10000012 0 scrollbar track (right edge, 16×68) UIElement_Scrollable::GetScrollbarPointer_ @0x473ec0
0x10000013 3 Field input bar bg 0x0600113A
0x10000014 6 Menu channel menu (Chat ▸) — 14 items (squelch + 13 channels) gmMainChatUI::InitTalkFocusMenu @0x4cdc50, HandleSelection @0x4cd540
0x10000016 0 (UIElement_Text) input — editable, one-line, focus sprite 0x060011AB ChatInterface::PostInit @0x4f3e86
0x10000017/18 3 input focus edges (sprite 0x06004D67)
0x10000019 0 Send button (0x06001915/pressed …16/ghost …34) ChatInterface::ListenToElementMessage @0x4f51ea
0x10000522525 0 numbered chat tabs 14 (left strip; Normal 0x06006218/Hi 0x06006219) gmMainChatUI::RecvNotice_SetPanelVisibility @0x4ccd80

Screenshot correction (user-provided retail ground truth, 2026-06-15): the four 0x10000522525 elements are the left-edge numbered chat tabs, NOT the "line/page scroll buttons" a research agent inferred from their 16×16 vertical geometry. The scrollbar (track + thumb + up/down) is on the right. The exact dat ids of the right-side scroll up/down buttons are located during Task D (likely children of track 0x10000012 not surfaced in the top-level dump).

BN field-name caveat: the decomp's m_chatEntry / m_chatLog / m_fCurrentOpacity names are applied inconsistently across functions (a Binary-Ninja artifact). The roles above are fixed by the decisive evidence — the Normal_focussed sprite is on 0x10000016 (only an editable field gets a focus state) and the multiline geometry is 0x10000011 — corroborated by both surviving research agents. Port by role, not by the C++ member name.

3.2 Key retail algorithms (cited)

InboundChatInterface::RecvNotice_DisplayFinalStringInfo @0x4f4640: append arg4 (prefix/timestamp) then arg3 (body) to the transcript via UIElement_Text::AppendStringInfoWithFont with per-chat-type color arg2 (color table built by BuildChatColorLookupTable). If glyph count > 0x2710 (10000), TruncateChatLog @0x4f4290 beheads from the top at a newline boundary. Bottom-pin: capture IsAtVerticalEnd before appending; if it was true, ScrollToPosition to the new end; else light the unread-text indicator.

SubmitChatInterface::HandleEnterKey @0x4f52d0 (fired by the Accept input-action, not raw \r — one-line mode drops the \r char) → ProcessCommand @0x4f5100: read input text, dispatch, push to m_InputHistory (cap 100, drop index 0 when full), reset history cursor to 0xFFFFFFFF, clear input. The Send button (0x10000019) is an alternate trigger via ListenToElementMessage.

ScrollUIElement_Scrollable: pixel offset m_iScrollableY, clamped to [0, contentHeight viewHeight] (SetScrollableXY @0x4740c0). thumbRatio = view/content clamped to 1, bar hidden when content ≤ view (UpdateScrollbarSize_ @0x4741a0). posRatio = scrollY/(contentview) (UpdateScrollbarPosition_ @0x473f20). Scroll quantum = one line-height (UIElement_Text::InqScrollDelta @0x4689b0); page = view height; wheel = 1 line per notch (HandleMouseWheel @0x471450).

Input — caret m_nCursorPos (glyph index); GlyphList::FindPixelsFromPos @0x472b40 = Σ glyph advances to the caret; midpoint-snap hit-test FindPosFromLineAndPixels @0x4732d0; ~1 Hz blink. Glyph advance = HorizontalOffsetBefore + Width + HorizontalOffsetAfter (signed bytes, Font::GetCharWidthA @0x4433f0) — already implemented by UiDatFont.GlyphAdvance. History: SelectCommandFromHistory (up=back, down=fwd), sentinel 0xFFFFFFFF = "not browsing".

Channel menuInitTalkFocusMenu @0x4cdc50 fills UIElement_Menu 0x10000014 with 14 items: item 0 = squelch toggle, items 113 = channels carrying attr 0x1000000B = channel enum (1=Say, 2=Tell/Target, 3=Emote, 4=Fellowship, 5=Patron, 6=Trade, 7=Allegiance, 80xD=area/custom). HandleSelection @0x4cd540 reads the enum, calls SetTalkFocus(enum), updates the label, marks the item selected.

4. Architecture (acdream)

Faithful structure: an importer builds the generic frame; a controller (ChatInterface+gmMainChatUI::PostInit analogue) binds behavior by element id and swaps the transcript/input placeholders for behavioral widgets. New classes live in src/AcDream.App/UI/ (widgets, GL-side) and src/AcDream.UI.Abstractions/ (the shared submit router).

Component Kind Retail analogue Responsibility
ChatWindowController new (App/UI/Layout/) ChatInterface + gmMainChatUI PostInit import 0x21000006; FindElement(id); swap transcript/input widgets; wire scrollbar/menu/send/max-min; route in/outbound
UiChatView extend (App/UI/) UIElement_Text (transcript) + UiDatFont? DatFont; dat-font measure/advance/draw; 1-line wheel quantum; keep bottom-pin + drag-select + Ctrl+C
UiChatInput new (App/UI/) UIElement_Text (editable) caret, insert/delete, 100-entry history, focus sprite swap, dat font, Action<string>? OnSubmit
UiScrollable new (App/UI/) UIElement_Scrollable pixel scroll math: ScrollY, ContentHeight, ViewHeight, ClampScroll, ThumbRatio, ThumbOffsetRatio, line/page delta
UiChatScrollbar new (App/UI/) composed scrollbar own the imported track/thumb/up-down sprites; size+place thumb from UiScrollable; clicks/drag → UiScrollable
UiChannelMenu new (App/UI/) UIElement_Menu dropdown popup of channels; selection → active ChatChannelKind; label reflects selection
ChatCommandRouter new (UI.Abstractions/Panels/Chat/) ProcessCommand shared submit: client-command intercept → unknown-verb guard → ChatInputParser.Parse(text, channel, lastTell, lastOutgoing)Publish(SendChatCmd)
UiDatFont no change Font already implements retail glyph advance

Why two widget classes (UiChatView + UiChatInput) when retail uses one UIElement_Text with mode bits: acdream's retained-mode widget layer predates D.2b; the behavioral contract (read-only multiline scroll vs editable one-line) is identical, only the class split differs. Accepted ADAPTATION divergence; both classes share the UiDatFont.GlyphAdvance measure seam so geometry is consistent.

Placeholder swap: the transcript (0x10000011) and input (0x10000016) render no background sprite of their own (bg comes from parent panels 0x10000010 / 0x10000013), so ChatWindowController reads each placeholder's rect + anchors, instantiates UiChatView / UiChatInput there, adds it to the placeholder's parent, and removes the placeholder. Mirrors GetChildRecursive(id) binding in ChatInterface::PostInit.

5. Data flow

  • Inbound: ChatLog → ChatVM.RecentLinesDetailed() (200-deep tail) → UiChatView.LinesProvider (per-ChatKind color via RetailChatColor). Pipeline unchanged. Bottom-pin + 10k cap are UiChatView/UiScrollable behavior.
  • Outbound: UiChatInput.OnSubmit(text)ChatCommandRouter.Submit(text, vm, commandBus, activeChannel)SendChatCmdLiveCommandBusWorldSession. activeChannel comes from UiChannelMenu.
  • Channel: UiChannelMenu selection → ChatWindowController._activeChannel (→ ChatInputParser default channel) + menu label update.
  • Scroll: transcript content height → UiScrollableUiChatScrollbar thumb; wheel/buttons/drag → UiScrollable.ScrollY → transcript draw offset.

6. Faithfulness decisions / divergence-register rows

Add on landing (category in parens):

  1. (Adaptation) Transcript + input are two classes (UiChatView/UiChatInput) not one mode-flagged UIElement_Text. Behavior identical.
  2. (Approximation) Transcript renders pre-split ChatLog lines 1:1; no in-element word-wrap at panel width. Symptom: long lines not re-wrapped on horizontal resize. file:line = UiChatView.cs.
  3. (Approximation) One color per display line, not per-glyph styled runs.
  4. (Stopgap) Numbered chat tabs render but don't switch / filter chat kinds.
  5. (Stopgap) Squelch toggle + clickable name-tags render/parse-absent.
  6. (Approximation) Single default translucency; no focused/unfocused opacity transition; default dat font face+size (no sm_nFontFace config).

Retire nothing (no existing register row is fixed by this work).

7. Build sequence (tasks for the plan)

Pipelineable where independent; ChatWindowController (G) and the GameWindow cutover (H) are the integration barrier.

  • A. ChatCommandRouter — extract the submit flow from ChatPanel into a pure UI.Abstractions helper; ChatPanel calls it; tests for client-command / unknown-verb / parse / publish parity. (UI.Abstractions; no GL.)
  • B. UiChatView dat-font seam — add UiDatFont? DatFont; prefer it in draw + HitChar advance + selection measure + LineHeight; change WheelLines 3→1; keep BitmapFont fallback. Tests: advance/hit-test with a synthetic dat font.
  • C. UiScrollable — port UIElement_Scrollable math (pixel clamp, thumb ratio/offset, line/page delta). Pure, fully unit-tested (no GL).
  • D. UiChatScrollbar — own imported track/thumb/up-down sprites; size+place thumb from UiScrollable; wheel/button/drag → scroll. Locate the right-side up/down button ids in the dat here.
  • E. UiChatInput — editable one-line widget: caret (MeasurePrefix = UiDatFont.MeasureWidth(text[..caret])), insert/delete, Home/End/arrows, 100-entry history with 1=live sentinel, focus sprite swap, OnSubmit. Tests for caret math + history.
  • F. UiChannelMenu — channel dropdown (port UIElement_Menu minimally); 13 channels → ChatChannelKind; selection event + label.
  • G. ChatWindowControllerLayoutImporter.Import(0x21000006); bind by id; swap transcript/input; wire scrollbar/menu/send/max-min; route inbound (ChatVM)
    • outbound (ChatCommandRouter); translucency.
  • H. GameWindow cutover — replace the hand-authored UiNineSlicePanel+UiChatView block with ChatWindowController; default bottom-left position + resizable; remove dead code; add divergence rows; dotnet build + dotnet test green.

8. Testing strategy

  • Pure/unit (no GL, no dats): ChatCommandRouter parity; UiScrollable clamp/thumb/delta golden values from the decomp; UiChatInput caret index ↔ pixel + history navigation; UiChatView dat-font advance/hit-test via the Func<char,FontCharDesc?> seam.
  • Layout/import (dat-free fixture): extend the importer fixture pattern with a chat_21000006.json tree (via ImportInfos) asserting the element→role map and rects.
  • Real-dat smoke: LayoutImporter.Import(0x21000006) against the live dat resolves the root + all bound ids before wiring (guarded, like the vitals smoke).
  • Visual acceptance (user): launch live ACDREAM_RETAIL_UI=1; compare to the retail screenshot — transcript scrolls, input types + sends, channel menu switches, Send works, scrollbar drags, window moves/resizes, translucency.

9. Acceptance criteria

  • Chat window is built from LayoutDesc 0x21000006 via LayoutImporter — no hand-authored chat rect remains in GameWindow.cs.
  • Transcript renders inbound chat in the dat font, per-ChatKind color, bottom-pinned, 10k-cap, mouse-wheel = 1 line/notch, drag-select + Ctrl+C kept.
  • Right-side scrollbar: thumb sizes to content, drag + up/down scroll the transcript.
  • Input: type, caret moves, backspace/delete, up/down history, Enter and the Send button both submit through ChatCommandRouter → wire.
  • Chat ▸ menu opens, lists channels, selection changes the outbound channel + updates the label.
  • Max/min toggles window height; window moves + resizes; translucent frame.
  • Every ported widget cites a class::method @address; every deferral has a divergence-register row.
  • dotnet build + dotnet test green; user visual sign-off.

10. Deferred / follow-ups (filed, not built)

In-element word-wrap (+ selection rework); numbered-tab switching + per-tab chat filtering; squelch; clickable name-tags; per-glyph styled runs; configurable font face/size; active/inactive opacity transition; the unidentified top-level Type-5 ListBox 0x1000001D (not bound by ChatInterface; likely a floaty/options element).