Swap ChatWindowController's imported main-chat LayoutDesc from the wrong 0x21000006 (an unrelated layout whose root and 800px resize bar appear nowhere in the EoR gameplay UI) to retail's ACTUAL main chat window, 0x2100006F (window root 0x10000600, authored 410x100 — confirmed by a direct DAT dump, found in dats.Local not dats.Portal). Every downstream compensation that existed only to paper over the wrong import is deleted: the hand-cropped 490px content width, the dropped 800px resize bar, the 9px transcript patch, the orphan-sibling pruning, the max/min-vs-scrollbar overlap shift, and the scrollbar top-reclaim. The window now mounts with RetailWindowChrome.Imported (0x2100006F's own 8 border/corner elements are its complete chrome) instead of the universal nine-slice wrapper. LayoutImporter/DatWidgetFactory gain a Type-9 (UIElement_Resizebar) case: UiResizeGrip decodes retail's exact four-bool BorderLocation algorithm (0x2A=bottom/0x2B=left/0x2C=right/0x2D=top, UIElement_Resizebar::StartMouseResizing @0x0046B7E0) into a ResizeEdges bitmask. A direct DAT dump established the true shape: only 7 of the 8 grip-position ids are Type 9 — the straight top-EDGE strip (0x1000069C) is a Type-2 Dragbar (move handle), not a Resizebar, because the main window has no title bar. UiRoot now gives a directly-hit grip's own edges priority over its generic proximity heuristic, and a directly-hit move handle the same priority over ambient proximity — so the plain top strip moves the window while its two corner grips resize it including the Y axis, and all 4 edges + 4 corners work everywhere else. This also fixes the reported "no diagonal cursor at corners" (CursorFeedbackController's existing RetailCursorCatalog cursor ids already matched the DAT exactly; they just never received a genuine diagonal edge combination) and "cannot grow in Y from the bottom-right corner" (the old NineSlice+crop mount's indirection is gone; the Imported mount uses the DAT's real minH=100/maxH=2000/minW=300/maxW=2000 directly). The 8 cosmetic "_Locked" border-art twins default hidden (register row AP-185 — retail's UiLocked-driven art swap between the two skins is not ported; UiRoot.UiLocked continues to gate the underlying interaction correctly either way). The 4 chat-window-1..4 indicator buttons import generically (visible, inert) for CH6b to wire. The two hand-drawn translucent-black tints on the transcript/input are removed now that their parent panels draw their own authored background sprites. Filed #366 (chat window's new-unseen-text indicator 0x1000048C is swallowed by UiText.ConsumesDatChildren, pre-existing and out of scope). Corrected the research doc's "all eight grips" claim against the direct DAT dump. Full Release suite: 12,317 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
745 lines
38 KiB
Markdown
745 lines
38 KiB
Markdown
# Campaign CH slice CH6 — retail chat-window SHELL research
|
||
|
||
**Date:** 2026-08-09
|
||
**Scope:** RESEARCH ONLY. How retail Asheron's Call constructs, shows, sizes,
|
||
persists and fades the chat windows — the *shell*, not the text pipeline.
|
||
The per-window text FILTER model is already decoded in
|
||
[`2026-08-09-chat-retail-color-table.md`](2026-08-09-chat-retail-color-table.md)
|
||
§4 and is not re-derived here.
|
||
|
||
**Primary sources**
|
||
- `docs/research/named-retail/acclient_2013_pseudo_c.txt` (Sept 2013 EoR build,
|
||
Binary Ninja pseudo-C, PDB-named)
|
||
- `docs/research/named-retail/acclient.h` (verbatim retail struct/enum defs)
|
||
- `docs/research/named-retail/symbols.json`
|
||
- `docs/research/named-retail/retail-default.keymap.txt`
|
||
- `docs/research/2026-06-25-retail-ui-layout-dump.json` — **an existing acdream
|
||
dump of the retail gameplay-UI top-level windows.** It carries the LayoutDesc
|
||
id, element ids, widget kinds and authored rects for every window below, and
|
||
is the geometric oracle used throughout §2.
|
||
- `references/ACE/Source/...` (server cross-check)
|
||
|
||
**Binary-Ninja field-name caveat (applies throughout).** BN's struct-field
|
||
attribution in `gm*ChatUI::PostInit` is shifted by one slot relative to the true
|
||
member order (the classic artifact class in
|
||
`claude-memory/feedback_bn_decomp_field_names.md`). Every id↔role binding in §2
|
||
was therefore re-derived from the **authored rectangles in the layout dump**,
|
||
which agree with the decomp's *call order* exactly once the one-slot shift is
|
||
undone. Where a claim rests on BN naming alone it is marked UNVERIFIED.
|
||
|
||
---
|
||
|
||
## 1. Window lifecycle — how windows 1–4 get created and shown
|
||
|
||
### 1.1 They are not created on demand. They are authored, always-resident, and toggled.
|
||
|
||
`gmGamePlayUI::SetupChildren @0x004E9EC0` builds the whole gameplay UI once:
|
||
|
||
```
|
||
004e9ed5 m_pGameplayUI = CreateAndAddRootElement(0x10000006, 0x10000495)
|
||
004e9f40 var_b0 = 0x10000505 // floating chat window 1
|
||
004e9f48 var_ac = 0x1000050E // floating chat window 2
|
||
004e9f50 var_a8 = 0x1000050F // floating chat window 3
|
||
004e9f58 var_a4 = 0x10000510 // floating chat window 4
|
||
004e9f6a hash("ID_Chat_Chat1_DefaultTitle") … "ID_Chat_Chat4_DefaultTitle"
|
||
004e9ff5 for i in 0..3:
|
||
004e9faf StringInfo::SetStringIDandTableEnum(&title, ids[i], 0x10000001)
|
||
004e9fbf el = UIElement::GetChildRecursive(m_pGameplayUI, windowIds[i])
|
||
004e9fd1 chat = el->DynamicCast(0x10000040) // gmFloatyChatUI
|
||
004e9fe9 chat->vtable[0x2AC](&title) // SetWindowTitle
|
||
```
|
||
|
||
So all four extra chat windows exist as **authored children of the gameplay-UI
|
||
root from the moment the game UI is built**. There is no window-manager `Create`
|
||
call, no per-window allocation at toggle time, and no floating-window registry.
|
||
Opening one is `SetVisible(true)`; closing one is `SetVisible(false)`.
|
||
|
||
Confirming class identity: `gmFloatyChatUI::GetUIElementType @0x004CE2B0` returns
|
||
`0x10000040`, and `gmFloatyChatUI::Register @0x004CE3D0` registers that element
|
||
class with the LayoutDesc element factory. `gmFloatyChatUI` derives from
|
||
`ChatInterface` and adds **no members at all** (`acclient.h:55144`), which is why
|
||
`m_eWindowID` (the first `ChatInterface` field, `acclient.h:54900`) is the only
|
||
per-window identity it carries.
|
||
|
||
### 1.2 Window identity comes from a LayoutDesc attribute, not from code
|
||
|
||
`ChatInterface::PostInit @0x004F3DD0`:
|
||
|
||
```
|
||
004f3de8 UIElement::GetAttribute_Enum(this, 0x1000007E, &this->m_eWindowID)
|
||
004f3df9 switch (m_eWindowID - 1) { … seed m_llTextTypeFilter defaults … }
|
||
```
|
||
|
||
**Attribute `0x1000007E` on the authored element IS the window id.** The main
|
||
chat window is id `0`; the floaties are `1..4` (the `PostInit` switch also has
|
||
arms for 5 and 8 — see the colour-table doc §4 for the seeded filter values).
|
||
|
||
### 1.3 The toggle is a KEYBIND, not a button
|
||
|
||
`docs/research/named-retail/retail-default.keymap.txt:150-153`, inside the
|
||
`ToggleWindows` action group:
|
||
|
||
```
|
||
ToggleFloatingChatWindow1 [ "" [ 0 DIK_1 ] 0x00000004 ]
|
||
ToggleFloatingChatWindow2 [ "" [ 0 DIK_2 ] 0x00000004 ]
|
||
ToggleFloatingChatWindow3 [ "" [ 0 DIK_3 ] 0x00000004 ]
|
||
ToggleFloatingChatWindow4 [ "" [ 0 DIK_4 ] 0x00000004 ]
|
||
```
|
||
|
||
`0x00000004` is the modifier mask (the same mask the `UseQuickSlot_14..18` rows
|
||
use for `Alt`+digit, versus `0x00000002` for the shift-digit quickslot rows).
|
||
So retail's default is **Alt+1 … Alt+4**, not bare 1–4.
|
||
UNVERIFIED: the exact modifier bit → key mapping (4 = Alt vs 4 = Ctrl). Cheapest
|
||
resolution: read `keymap-default.txt`'s modifier legend, or one cdb breakpoint on
|
||
`UIElementManager::KeyPressEvent`. The *mechanism* below is independent of this.
|
||
|
||
The dispatch chain is fully generic — there is no chat-specific code in it:
|
||
|
||
```
|
||
UIElementManager::KeyPressEvent @0x0045C300
|
||
0045c347 DoVisibilityToggleAction(this, actionId)
|
||
|
||
UIElementManager::DoVisibilityToggleAction @0x0045B660
|
||
0045b680 look up actionId in m_elementInputActionListenerTable
|
||
0045b6c1 for each registered element:
|
||
BroadcastElementMessage(element, 0x31, actionId, 0)
|
||
```
|
||
|
||
and the registration side is a plain authored property:
|
||
|
||
```
|
||
UIElement::OnSetAttribute (property switch) @0x00462D80
|
||
004631d7 case 0x24: // "input action" property
|
||
004631fb UIElementManager::RegisterElementForInputAction(
|
||
UIElementManager::s_pInstance, enumValue, this)
|
||
```
|
||
|
||
**So: LayoutDesc property `0x24` on the window element declares the input action
|
||
that toggles it; the manager broadcasts element message `0x31` ("toggle
|
||
visibility") to every element registered for that action.** The same mechanism
|
||
serves `ToggleInventoryPanel`, `ToggleSpellbookPanel`, etc.
|
||
|
||
UNVERIFIED: the numeric action-enum values behind
|
||
`ToggleFloatingChatWindow1..4` (the keymap file stores names, and the property
|
||
`0x24` value lives in the LayoutDesc, not in code). Cheapest resolution: dump
|
||
property `0x24` for elements `0x10000505/0x1000050E/0x1000050F/0x10000510` from
|
||
LayoutDesc `0x21000070`/gameplay root — one `ui-studio --dump` pass. Not needed
|
||
to implement CH6: acdream binds its own actions.
|
||
|
||
### 1.4 The 1/2/3/4 buttons in the main window are STATE MIRRORS, not the toggle
|
||
|
||
They exist and they are authored (§2.1, elements `0x10000522`–`0x10000525`), but
|
||
the code path is one-directional — window visibility drives the buttons:
|
||
|
||
```
|
||
gmGamePlayUI::ListenToElementMessage @0x004E9CE0
|
||
004e9cee if (idMessage == 0x18) // visibility changed
|
||
004e9d29 if (idElement == 0x10000505 ||
|
||
(idElement > 0x1000050D && idElement <= 0x10000510))
|
||
004e9d4c CM_UI::SendNotice_SetPanelVisibility(idElement, elementVisibleBit)
|
||
|
||
gmMainChatUI::RecvNotice_SetPanelVisibility @0x004CCD80
|
||
004ccd9c 0x10000505 -> child 0x10000522
|
||
004ccda3 0x1000050E -> child 0x10000523
|
||
004ccdaa 0x1000050F -> child 0x10000524
|
||
004ccdb1 0x10000510 -> child 0x10000525
|
||
004ccdbf child = GetChildRecursive(this, mapped)
|
||
004ccdcd visible ? child->SetState(6) : child->SetState(1)
|
||
```
|
||
|
||
State 6 = "on/depressed", state 1 = "normal". No handler anywhere in the binary
|
||
switches on `0x10000522..0x10000525` as a *source* of a click — `grep` over the
|
||
whole pseudo-C returns only `gmMainChatUI::RecvNotice_SetPanelVisibility`.
|
||
|
||
UNVERIFIED (and the one place I would not guess): whether clicking those four
|
||
buttons does anything in retail at all. Two readings are consistent with the
|
||
decomp — (a) they are pure indicators, and (b) they carry the same LayoutDesc
|
||
property `0x24` input-action value as the windows themselves, so a click routes
|
||
through the generic `UIElement` action path rather than through any chat code.
|
||
(b) is the more likely reading given §1.3's generic mechanism. Cheapest
|
||
resolution: the same LayoutDesc property dump as above, reading property `0x24`
|
||
on `0x10000522`–`0x10000525`. **For CH6 it is safe to wire both: keybind AND
|
||
button click both call the same toggle**, because retail's observable behaviour
|
||
(button lights up iff window is visible) is satisfied either way.
|
||
|
||
### 1.5 Closing a floaty window from its own title bar
|
||
|
||
```
|
||
gmFloatyChatUI::ListenToElementMessage @0x004CE330
|
||
004ce344 if (idMessage == 1 && idElement == 0x1000052A)
|
||
004ce346 this->vtable[…]( 0 ) // SetVisible(false)
|
||
```
|
||
|
||
Element `0x1000052A` is the 14×14 close button at (230, 86) in the floaty
|
||
layout (§2.2). `idMessage == 1` is "clicked".
|
||
|
||
### 1.6 The lock-UI cosmetic swap
|
||
|
||
`gmFloatyMainChatUI::UpdateLockedStatus @0x004D23D0` (driven by global message
|
||
`0x0D` via `gmFloatyMainChatUI::ListenToGlobalMessage @0x004D2940`) reads
|
||
`PlayerModule::LockUI` and swaps the 8 live border/corner elements for their 8
|
||
`_Locked` cosmetic twins. When the UI is locked the *interactive* Resizebars are
|
||
hidden and the inert art is shown — that is how retail disables resizing without
|
||
touching the resize code.
|
||
|
||
---
|
||
|
||
## 2. LayoutDesc geometry
|
||
|
||
### 2.1 Main chat window — LayoutDesc `0x2100006F`
|
||
|
||
Window element `0x10000601` (this is the id `gmGamePlayUI` looks up and the id
|
||
`SaveScreenLayout @0x004EAD50` writes as `<CHAT>`), layout root element
|
||
`0x10000600`, **authored extent 410 × 100**.
|
||
|
||
Full authored tree (from `2026-06-25-retail-ui-layout-dump.json`; rects are
|
||
absolute in the dump's 640×480 space, i.e. root at 0,0):
|
||
|
||
| element | rect (x,y,w,h) | role |
|
||
|---|---|---|
|
||
| `0x10000600` | 0,0,410,100 | window root (Group) |
|
||
| `0x10000693` | 0,0,5,5 | TL corner — **Locked twin** |
|
||
| `0x10000694` | 5,0,400,5 | top edge — Locked twin |
|
||
| `0x10000695` | 405,0,5,5 | TR corner — Locked twin |
|
||
| `0x10000696` | 0,5,5,90 | left edge — Locked twin |
|
||
| `0x10000697` | 0,95,5,5 | BL corner — Locked twin |
|
||
| `0x10000698` | 5,95,400,5 | bottom edge — Locked twin |
|
||
| `0x10000699` | 405,95,5,5 | BR corner — Locked twin |
|
||
| `0x1000069A` | 405,5,5,90 | right edge — Locked twin |
|
||
| `0x1000069B` | 0,0,5,5 | **TL corner — live Resizebar** |
|
||
| `0x1000069C` | 5,0,400,5 | **top edge — live Resizebar** |
|
||
| `0x1000069D` | 405,0,5,5 | **TR corner — live Resizebar** |
|
||
| `0x1000069E` | 0,5,5,90 | **left edge — live Resizebar** |
|
||
| `0x1000069F` | 0,95,5,5 | **BL corner — live Resizebar** |
|
||
| `0x100006A0` | 5,95,400,5 | **bottom edge — live Resizebar** |
|
||
| `0x100006A1` | 405,95,5,5 | **BR corner — live Resizebar** |
|
||
| `0x100006A2` | 405,5,5,90 | **right edge — live Resizebar** |
|
||
| `0x10000010` | 5,5,400,73 | transcript panel (Sprite) |
|
||
| `0x10000011` | 21,5,368,73 | transcript text (Group) |
|
||
| `0x1000048C` | 21,62,16,16 | new-unseen-text indicator (Button) |
|
||
| `0x10000012` | 389,5,16,73 | scrollbar column |
|
||
| `0x10000364/65/66`, `0x10000071`, `0x10000072` | — | scrollbar thumb pieces + up/down buttons |
|
||
| `0x1000046F` | 368,5,16,16 | max/min toggle (Button) |
|
||
| `0x10000522` | 5,5,16,16 | **chat-window-1 indicator (Button)** |
|
||
| `0x10000523` | 5,22,16,16 | **chat-window-2 indicator** |
|
||
| `0x10000524` | 5,39,16,16 | **chat-window-3 indicator** |
|
||
| `0x10000525` | 5,56,16,16 | **chat-window-4 indicator** |
|
||
| `0x10000013` | 5,78,400,17 | input row (Sprite) |
|
||
| `0x10000014` | 5,78,46,17 | talk-focus menu button |
|
||
| `0x10000015` | 5,78,46,17 | talk-focus menu group |
|
||
| `0x10000016` | 51,78,306,17 | **chat entry field** |
|
||
| `0x10000017` / `0x10000018` | 1px | entry field left/right rails |
|
||
| `0x10000019` | 359,78,46,17 | Send button |
|
||
|
||
Cross-checks that pin this layout to the code, independent of the dump:
|
||
- `gmMainChatUI::HandleMaximizeButton @0x004CCE50` looks up `0x1000046F`.
|
||
- `gmMainChatUI::RecvNotice_SetPanelVisibility @0x004CCD80` writes
|
||
`0x10000522`–`0x10000525`.
|
||
- `gmFloatyMainChatUI::PostInit @0x004D2670` binds exactly the 16 ids
|
||
`0x10000693`–`0x100006A2`, and casts eight of them via `DynamicCast(9)` —
|
||
element type **9 is `UIElement_Resizebar`**
|
||
(`UIElement_Resizebar::Register @0x0046B920` → `RegisterElementClass(9, …)`).
|
||
|
||
**The 1/2/3/4 buttons are on the LEFT edge, stacked vertically** — not a tab row.
|
||
|
||
### 2.2 Floating chat windows 1–4 — LayoutDesc `0x2100005B`
|
||
|
||
All four windows instantiate **the same** LayoutDesc; only the window element id
|
||
and the `0x1000007E` window-id attribute differ. Layout root `0x100004F7`,
|
||
**authored extent 250 × 108**.
|
||
|
||
| element | rect (x,y,w,h) | role |
|
||
|---|---|---|
|
||
| `0x100004F7` | 0,80,250,108 | window root (Group) |
|
||
| `0x100004FC` | 0,80,5,5 | TL corner |
|
||
| `0x1000000F` | 5,80,240,5 | top edge |
|
||
| `0x100004FE` | 245,80,5,5 | TR corner |
|
||
| `0x100004D2` | 0,85,5,98 | left edge |
|
||
| `0x10000501` | 0,183,5,5 | BL corner |
|
||
| `0x100004D4` | 5,183,240,5 | bottom edge |
|
||
| `0x10000503` | 245,183,5,5 | BR corner |
|
||
| `0x100004D3` | 245,85,5,98 | right edge |
|
||
| `0x100004D9` | 5,85,240,16 | **title bar** |
|
||
| `0x10000528` | 5,100,240,5 | title/content divider |
|
||
| `0x10000529` | 5,85,240,20 | title-bar group (drag handle) |
|
||
| `0x1000052A` | 230,86,14,14 | **close button** |
|
||
| `0x10000010` | 5,105,224,60 | transcript panel |
|
||
| `0x10000011` | 5,105,224,60 | transcript text |
|
||
| `0x1000048C` | 5,169,16,16 | new-unseen-text indicator |
|
||
| `0x10000012` | 229,105,16,60 | scrollbar column (+ children) |
|
||
| `0x10000509` | 5,165,240,18 | input row |
|
||
| `0x10000016` | 5,165,202,18 | chat entry field |
|
||
| `0x1000052B`, `0x100002B5`, `0x10000209`, `0x1000020A`, `0x1000020B` | — | entry-field frame rails |
|
||
| `0x10000019` | 207,165,38,18 | Send button |
|
||
|
||
Independent code cross-check: `gmFloatyChatUI::SetWindowTitle @0x004CEAA0` does
|
||
`GetChildRecursive(this, 0x100004D9)` and `SetStringInfo` on it — exactly the
|
||
240×16 title strip above.
|
||
|
||
A floaty chat window has **no** talk-focus menu, **no** max/min button and **no**
|
||
1/2/3/4 indicators. It has a title bar (which the main window does not) and a
|
||
close button.
|
||
|
||
### 2.3 How retail encodes resizability — there are no "sizable edge" flags
|
||
|
||
Retail does **not** put a bitmask on the window. Each draggable edge and corner
|
||
is its own child element of type 9 (`UIElement_Resizebar`), and four authored
|
||
BOOL properties on that child say which border it drives:
|
||
|
||
`UIElement_Resizebar::StartMouseResizing @0x0046B7E0`
|
||
|
||
```
|
||
0046b7f8 GetAttribute_Bool(this, 0x2C, &bRight)
|
||
0046b806 GetAttribute_Bool(this, 0x2B, &bLeft)
|
||
0046b814 GetAttribute_Bool(this, 0x2D, &bTop)
|
||
0046b822 GetAttribute_Bool(this, 0x2A, &bBottom)
|
||
|
||
if (bRight) border = bTop ? BORDER_UR : (bBottom ? BORDER_LR : BORDER_RIGHT)
|
||
else if (bLeft) border = bTop ? BORDER_UL : (bBottom ? BORDER_LL : BORDER_LEFT)
|
||
else if (bTop) border = BORDER_TOP
|
||
else if (bBottom)border = BORDER_BOTTOM
|
||
|
||
0046b88e this->m_mousePressed = 1
|
||
0046b89f UIElement::StartResizing(parent, border, pt.x, pt.y)
|
||
0046b8a4 this->SetState(3) // "pressed" art
|
||
```
|
||
|
||
`BorderLocation` (`acclient.h:4327`):
|
||
`BORDER_NONE=0, BORDER_UL=1, BORDER_TOP=2, BORDER_UR=3, BORDER_RIGHT=4,
|
||
BORDER_LR=5, BORDER_BOTTOM=6, BORDER_LL=7, BORDER_LEFT=8`.
|
||
|
||
So the property map is **`0x2A` = bottom, `0x2B` = left, `0x2C` = right,
|
||
`0x2D` = top**, and a corner grip simply sets two of them.
|
||
|
||
Drag lifecycle (`UIElement_Resizebar::ListenToElementMessage @0x0046B930`):
|
||
message `0x1C` (mouse down, `dwParam1 == 7` = left button) → `StartMouseResizing`
|
||
+ parent's vtable+0x60; `0x1E` (mouse move) → parent's vtable+0x4C
|
||
(`MouseResizeElement @0x00461130`); `0x1D` (mouse up) → parent
|
||
vtable+0x64 + `StopMouseResizing` → `UIElement::StopResizing @0x0045FD60`.
|
||
|
||
**Both chat layouts author all eight grips.** Retail is resizable from every
|
||
edge and every corner, including the top and the top corners, on both the main
|
||
and the floating chat windows.
|
||
|
||
**CORRECTED 2026-08-10 at Campaign CH slice CH6a implementation.** This claim
|
||
is wrong for the main window's plain TOP EDGE specifically, established by a
|
||
direct `ElementDesc.Type` dump of the installed DAT (ground truth, not a
|
||
decomp reading) of the 8 ids `0x1000069B`-`0x100006A2`: 7 of them are
|
||
Type 9 (`UIElement_Resizebar`) as claimed, but `0x1000069C` — the straight
|
||
top-edge strip between the two top corners — is Type **2**
|
||
(`UIElement_Dragbar`), not Type 9. So retail's main chat window is
|
||
resizable from every edge and corner EXCEPT the plain top strip, which is a
|
||
MOVE handle instead (there is no title bar, so the top strip does double
|
||
duty as the drag affordance). The two top CORNERS (`0x1000069B` UL,
|
||
`0x1000069D` UR) are still genuine Resizebar grips carrying the top bool, so
|
||
dragging from a corner still resizes the Y/top axis — only the straight
|
||
edge in between does not. This is fully consistent with, and explains, the
|
||
`UIElement_Resizebar::StartMouseResizing` cursor media dump: `0x1000069C`
|
||
carries cursor `0x06006119` (the four-arrow MOVE cursor, matching
|
||
`WindowMove`), not one of the two diagonal or the vertical resize cursor
|
||
ids the seven true grips carry. Not independently re-verified for the
|
||
floating-window layout `0x2100005B` (CH6b's scope); assume it needs the
|
||
same direct-dump check rather than trusting this row for that layout too.
|
||
|
||
### 2.4 Min/max extents
|
||
|
||
Retail reads them as ordinary integer attributes on the window element:
|
||
`gmMainChatUI::HandleMaximizeButton @0x004CCE50` uses
|
||
`GetAttribute_Int(this, 0x3C, &maxH)` and `GetAttribute_Int(this, 0x3E, &minH)`.
|
||
acdream already consumes the same family — `RetailWindowFrame.ResolveConstraint`
|
||
(`src/AcDream.App/UI/Layout/RetailWindowFrame.cs:160-169`) maps
|
||
`0x3C`=maxH, `0x3D`=maxW, `0x3E`=minH, `0x3F`=minW. No change needed.
|
||
|
||
---
|
||
|
||
## 3. Opacity — the two values
|
||
|
||
Named constants in the binary's read-only data at `0x007A8D50`:
|
||
|
||
```
|
||
007a8d50 uint32_t const Option_TextType_Property = 0x1000007F
|
||
007a8d54 uint32_t const Option_DefaultOpacity_Property = 0x10000080
|
||
007a8d58 uint32_t const Option_ActiveOpacity_Property = 0x10000081
|
||
```
|
||
|
||
| id | name | type | scope | meaning |
|
||
|---|---|---|---|---|
|
||
| `0x10000080` | `Option_DefaultOpacity_Property` | float | **global** (`InqOption`, not per-window) | opacity when the window's text entry does NOT have focus |
|
||
| `0x10000081` | `Option_ActiveOpacity_Property` | float | **global** | opacity while the window's text entry HAS focus |
|
||
|
||
Read path (`gmFloatyChatUI::UpdateFromPlayerModule @0x004CE3F0`,
|
||
identical in `gmFloatyMainChatUI::UpdateFromPlayerModule @0x004D2970`):
|
||
|
||
```
|
||
004ce42b if (PlayerModule::InqOption(pm, 0x10000080, &prop) && prop->InqFloat(&v))
|
||
004ce445 ChatInterface::SetDefaultOpacity(this, v)
|
||
004ce465 if (PlayerModule::InqOption(pm, 0x10000081, &prop) && prop->InqFloat(&v))
|
||
004ce47f ChatInterface::SetActiveOpacity(this, v)
|
||
```
|
||
|
||
Live-update path (`gmFloatyMainChatUI::RecvNotice_GameplayOptionChanged
|
||
@0x004D25A0`) switches on the same two ids and forwards anything else to
|
||
`ChatInterface::RecvNotice_GameplayOptionChanged @0x004F30E0` (which handles the
|
||
`0x1000007F` filter).
|
||
|
||
They are also settable per-element from the LayoutDesc:
|
||
`ChatInterface::OnSetAttribute @0x004F3F60` accepts `0x10000080` / `0x10000081`
|
||
as element attributes and **falls back to `0x3F800000` (= 1.0f)** when the
|
||
property value cannot be read (`004f3fb2`, `004f3f84`). That 1.0f is the only
|
||
default the code itself carries.
|
||
|
||
The options UI wires them to two linked sliders — `gmChatOptionsUI::InitOptions
|
||
@0x0049FC60`:
|
||
|
||
```
|
||
0049fcc0 UIOption_Slider::SetGameplayOptionProperty(slider1, 0x10000080)
|
||
0049fd1a UIOption_Slider::SetGameplayOptionProperty(slider2, 0x10000081)
|
||
0049fd5c DualHash<UIOption_Slider*,UIOption_Slider*>::add(&m_hashSliderLinks, &slider1, &slider2)
|
||
```
|
||
|
||
The `DualHash` link is why the two sliders track each other in the retail
|
||
options panel, and it is mirrored in code:
|
||
`ChatInterface::SetDefaultOpacity @0x004F3BC0` calls `SetActiveOpacity` when
|
||
active < default, and `SetActiveOpacity @0x004F3C40` calls `SetDefaultOpacity`
|
||
when default > active. **Invariant: `activeOpacity >= defaultOpacity` always.**
|
||
|
||
Which one is applied right now (`SetDefaultOpacity @0x004F3BC0`,
|
||
`SetActiveOpacity @0x004F3C40`):
|
||
|
||
```
|
||
if (this window's root is the active element
|
||
&& GetFocusDescendant(activeElement) == m_chatEntry)
|
||
m_fCurrentOpacity = m_fActiveOpacity
|
||
else m_fCurrentOpacity = m_fDefaultOpacity
|
||
```
|
||
|
||
and application is one call on the window's own render surface —
|
||
`ChatInterface::SetOpacity @0x004F3120`:
|
||
|
||
```
|
||
004f3124 this->m_fCurrentOpacity = v
|
||
004f312a obj = m_object ?: UIRegion::GetObjectA(m_parent)
|
||
004f314b surface = obj->vtable[0x1C]() // get render surface
|
||
004f3156 surface->vtable[0x48](v) // set surface alpha
|
||
```
|
||
|
||
**Retail fades the whole composited window surface — chrome, backgrounds AND
|
||
text — with one alpha, not a per-widget background tint.** This is the shape the
|
||
future acdream user setting should take.
|
||
|
||
UNVERIFIED: retail's *shipped default values* for the two sliders. They are not
|
||
constants in code (the only in-code fallback is 1.0f); they arrive from the
|
||
server's `GameplayOptions` blob or from the options page's authored slider
|
||
defaults. Cheapest resolution: one cdb breakpoint on
|
||
`ChatInterface::SetDefaultOpacity` at login and read `st(0)` — or dump the
|
||
slider template defaults from the options LayoutDesc (`0x2100002B`).
|
||
|
||
---
|
||
|
||
## 4. Persistence — how filters, geometry, visibility and title survive
|
||
|
||
### 4.1 The per-window option structure
|
||
|
||
Every per-window chat setting lives inside ONE global gameplay option, an
|
||
array indexed by `windowId - 1`. `PlayerModule::GetChatOptionStructure
|
||
@0x005D5300`:
|
||
|
||
```
|
||
005d5326 find option 0x1000008C in m_colGameplayOptions // the ARRAY
|
||
005d5360 … if absent: SetPropertyName(&p, 0x1000008C); hash.add(p)
|
||
005d5473 if (arrayProp->type == 0x11 && (windowId-1) < arrayProp->capacity)
|
||
005d54a8 SetPropertyName(&elem, 0x1000008B) // the ELEMENT
|
||
005d54b3 for i in currentCount..=(windowId-1): append elem
|
||
005d553b return arrayProp->vtable[0x10C](windowId - 1) // entry
|
||
```
|
||
|
||
- **`0x1000008C`** — the per-window option ARRAY (one entry per chat window).
|
||
- **`0x1000008B`** — the property name of each ARRAY ELEMENT (a nested bag).
|
||
|
||
`PlayerModule::InqChatWindowOption @0x005D5540` / `SetChatWindowOption
|
||
@0x005D5570` are thin `(windowId, propertyId)` accessors over that structure.
|
||
|
||
### 4.2 The complete `InqChatWindowOption` property family
|
||
|
||
| id | type | written by | read by |
|
||
|---|---|---|---|
|
||
| `0x1000007F` | bitfield64 | chat options UI (`gmChatOptionsUI::AddCheckboxBitfield64Option @0x0049EDA0`) | `ChatInterface::UpdateFromPlayerModule @0x004F3920`; live via `RecvNotice_GameplayOptionChanged @0x004F30E0` |
|
||
| `0x10000086` | int — window **X** | `gmFloatyChatUI::MoveTo @0x004CE840:004ce892`; `gmFloatyMainChatUI::MoveTo @0x004D2D10:004d2e06` | `UpdateFromPlayerModule` → `MoveTo(x,y)` |
|
||
| `0x10000087` | int — window **Y** | same `MoveTo` sites, `:004ce8da` / `:004d2e4e` | same |
|
||
| `0x10000088` | int — **width** | `gmFloatyChatUI::ResizeTo @0x004CE6D0:004ce722`; `gmFloatyMainChatUI::ResizeTo @0x004D2C00:004d2c5e` | `UpdateFromPlayerModule` → `ResizeTo(w,h)` |
|
||
| `0x10000089` | int — **height** | same `ResizeTo` sites, `:004ce770` / `:004d2cac` | same |
|
||
| `0x1000008A` | bool — **visible / open** | `gmFloatyChatUI::SetVisible @0x004CE9B0:004ce9ff` | `UpdateFromPlayerModule` → `SetVisible` |
|
||
| `0x1000008D` | StringInfo — **window title** | `gmFloatyChatUI::SetWindowTitle @0x004CEAA0:004ceb14` | `UpdateFromPlayerModule @0x004CE3F0:004ce63a` |
|
||
|
||
Two guards worth porting verbatim:
|
||
|
||
1. Every write is gated on `m_eWindowID != 0` — **the main chat window (id 0)
|
||
never persists geometry, visibility, title or filter.** Only windows 1..4 do.
|
||
2. Position/size restore is skipped entirely when
|
||
`CPlayerSystem::GetPlayerSystem()->m_layoutFromFile != 0`
|
||
(`004ce4a8`, `004d2a25`) — i.e. a `LoadScreenLayout` file wins over the
|
||
server-side option blob. Visibility and title still restore in that case.
|
||
|
||
`gmFloatyMainChatUI::MoveTo @0x004D2D10` additionally clamps the window inside
|
||
its parent before delegating (`004d2d53`–`004d2dbb`), so a saved position from a
|
||
larger resolution can never strand the window off-screen.
|
||
|
||
### 4.3 The local screen-layout file (a second, independent persistence path)
|
||
|
||
`gmGamePlayUI::SaveScreenLayout @0x004EAD50` / `LoadScreenLayout @0x004EA8F0`
|
||
write a plain-text file (path built by `CreateScreenLayoutPath @0x004EA690`)
|
||
with one line per window:
|
||
|
||
```
|
||
<SBOX> X:%d Y: %d W: %d H: %d // 0x1000049A
|
||
<CHAT> X:%d Y: %d W: %d H: %d // 0x10000601 ← main chat
|
||
<FCH1> X:%d Y: %d W: %d H: %d // 0x10000505
|
||
<FCH2> … // 0x1000050E
|
||
<FCH3> … // 0x1000050F
|
||
<FCH4> … // 0x10000510
|
||
```
|
||
|
||
`LoadScreenLayout` matches the 4-character tags back to element ids
|
||
(`004eaa91`, `004eaab8`, `004eaadf`, `004eab06`). This is retail's
|
||
"save/load UI layout" feature and is the `m_layoutFromFile` flag's source.
|
||
Notably it is the ONLY path that persists the **main** chat window's geometry.
|
||
|
||
### 4.4 The wire
|
||
|
||
`m_colGameplayOptions` is packed whole into the character-options blob.
|
||
Server side, ACE treats it as **opaque bytes**:
|
||
|
||
- outbound: `references/ACE/Source/ACE.Server/Network/GameEvent/Events/GameEventPlayerDescription.cs:349-350, 393-394` —
|
||
sets `CharacterOptionDataFlag.GameplayOptions` (`0x00000200`) and writes
|
||
`Character.GameplayOptions` verbatim.
|
||
- inbound: `references/ACE/Source/ACE.Server/Network/GameAction/Actions/GameActionSetCharacterOptions.cs:183-190` —
|
||
*"This is the last message... So it should be all that is left"* — reads the
|
||
remaining bytes and calls `SetCharacterGameplayOptions(bytes)`.
|
||
- storage: `references/ACE/Source/ACE.Database/Models/Shard/Character.cs:49` —
|
||
`public byte[] GameplayOptions`.
|
||
|
||
**ACE does not parse chat-window options at all.** It stores and echoes the
|
||
blob. Consequences for acdream:
|
||
- Anything acdream writes into that blob will round-trip through ACE unchanged.
|
||
- Nothing on the server validates it, so acdream owns the format entirely —
|
||
which means acdream must match retail's `PropertyCollection` packing exactly
|
||
if a retail client and acdream are ever to share a character.
|
||
- Until acdream can pack it, the cheapest correct behaviour is **local**
|
||
persistence (the existing `SettingsStore` window-layout path), with the wire
|
||
round-trip deferred.
|
||
|
||
---
|
||
|
||
## 5. acdream inventory — what exists, what is missing
|
||
|
||
### 5.1 What we mount today
|
||
|
||
`RetailUiRuntime.MountChat()` — `src/AcDream.App/UI/RetailUiRuntime.cs:670-736`:
|
||
|
||
```
|
||
676 LayoutImporter.ImportInfos(dats, ChatWindowController.LayoutId) // 0x21000006
|
||
691 ChatWindowController.Bind(info, layout, …)
|
||
708 RetailWindowFrame.Mount(Host.Root, root, …)
|
||
714 WindowName = WindowNames.Chat
|
||
715 Chrome = RetailWindowChrome.NineSlice
|
||
718 ContentWidth = 490f
|
||
728 ResizableEdges = ResizeEdges.Left | ResizeEdges.Right | ResizeEdges.Bottom
|
||
729 Opacity = 0.75f
|
||
```
|
||
|
||
`ChatWindowController` — `src/AcDream.App/UI/Layout/ChatWindowController.cs`:
|
||
|
||
- `:29` `LayoutId = 0x21000006`, `:33` `RootId = 0x1000000E`
|
||
- `:34` `ResizeBarId = 0x1000000F` — **dropped** at `:204-205`
|
||
- `:35-42` transcript panel / transcript / track / input bar / menu / input /
|
||
send / max-min ids
|
||
- `:228`, `:237` `BackgroundColor = (0,0,0,0.35)` on transcript and input
|
||
- `:383-426` `ToggleMaximize` — a faithful port of
|
||
`gmMainChatUI::HandleMaximizeButton @0x004CCE50`
|
||
|
||
### 5.2 Gap list
|
||
|
||
**G1 and G2 are CLOSED as of Campaign CH slice CH6a (2026-08-10).**
|
||
`ChatWindowController.LayoutId`/`RootId` now import `0x2100006F`/`0x10000600`;
|
||
the crop/rebase/orphan-pruning compensations are deleted; `LayoutImporter`
|
||
gained a Type-9 case (`UiResizeGrip`); `UiRoot` gives a directly-hit grip's
|
||
own edges priority over its generic proximity heuristic; the main window
|
||
mounts with `RetailWindowChrome.Imported` (0x2100006F's own border art is
|
||
its chrome). The two subsections below are kept verbatim as the historical
|
||
diagnosis — do not re-run this investigation.
|
||
|
||
**G1 — Wrong LayoutDesc for the main window.**
|
||
`ChatWindowController.cs:29` imports `0x21000006` with root `0x1000000E`.
|
||
Retail's EoR main chat window is **LayoutDesc `0x2100006F`**, window element
|
||
`0x10000601`, root `0x10000600`, 410 × 100 (§2.1), evidenced three independent
|
||
ways: the runtime layout dump, `gmGamePlayUI::SetupChildren`/`SaveScreenLayout`
|
||
element ids, and `gmMainChatUI`'s own `0x1000046F` / `0x10000522`–`0x10000525`
|
||
child lookups. `0x21000006` is a different (older/standalone) chat layout — its
|
||
root `0x1000000E` and its 800px-wide resize bar `0x1000000F` appear nowhere in
|
||
the EoR gameplay UI, and `ChatWindowController.cs:185-193` already documents
|
||
that it drags along stray unparented siblings (`0x1000001C/1D/1E`,
|
||
`0x10000526`) that had to be orphaned by hand. Every symptom in the user's
|
||
report (3) — stray geometry, hand-cropped 490px content width, a dropped
|
||
800px resize bar, a 9px hole patched by growing the transcript panel at
|
||
`:210-211` — is downstream of this single choice.
|
||
UNVERIFIED: the exact provenance of `0x21000006`. Cheapest resolution:
|
||
`AcDream.App ui-studio --layout 0x21000006 --dump` next to
|
||
`--layout 0x2100006F --dump` and diff the element sets — one command, no
|
||
connected session.
|
||
|
||
**G2 — Only three edges resize, and the resize model is not retail's.**
|
||
`RetailUiRuntime.cs:728` sets `ResizableEdges = Left | Right | Bottom`,
|
||
deliberately excluding `Top` because the authored resize bar was dropped (G1).
|
||
`UiRoot.HitEdges` (`src/AcDream.App/UI/UiRoot.cs:999-1010`) does support
|
||
corners — a corner is just two bits — so with `Top` masked out the top-left
|
||
and top-right corners are dead and only the two bottom corners work. That is
|
||
exactly the user's report (2). Retail authors **eight** grips (§2.3) with
|
||
per-grip bool properties `0x2A`/`0x2B`/`0x2C`/`0x2D`. Additionally,
|
||
`LayoutImporter` has no concept of element type 9 (`UIElement_Resizebar`) — grep
|
||
for `Resizebar` in `src/AcDream.App/UI` returns nothing — so authored grips
|
||
would be imported as inert sprites today.
|
||
|
||
**G3 — Window opacity is completely inert.**
|
||
`RetailWindowFrame.cs:157` sets `outerFrame.Opacity`, and
|
||
`UiElement.DrawSelfAndChildren` (`src/AcDream.App/UI/UiElement.cs:465`) and
|
||
`DrawOverlays` (`:513`) push it onto `UiRenderContext`'s alpha stack. But
|
||
**`UiRenderContext.AlphaMod` (`src/AcDream.App/UI/UiRenderContext.cs:55`) has
|
||
zero consumers** — a repo-wide grep over `src/` and `tests/` returns only its
|
||
own declaration plus one `PushAlphaAbsolute` call in `UiMenu.cs:293`. No sprite,
|
||
rect or text draw ever multiplies by it. So `Opacity = 0.75f` at
|
||
`RetailUiRuntime.cs:729` changes nothing, and the only translucency the chat
|
||
window has is the two hard-coded `(0,0,0,0.35)` background tints at
|
||
`ChatWindowController.cs:228` and `:237` plus whatever alpha is baked into the
|
||
DAT chrome textures. That is the user's report (3). Retail applies ONE alpha to
|
||
the whole composited window surface including text (§3).
|
||
|
||
**G4 — No multi-window support of any kind.**
|
||
`WindowNames` has a single `Chat` entry; `RetailUiRuntime.MountChat()` mounts
|
||
exactly one window; there is no window-id concept, no `0x1000007E` attribute
|
||
read, no filter state, no `0x10000522`–`0x10000525` binding, and no
|
||
`ToggleFloatingChatWindow1..4` input action. Grep for `ToggleFloatingChat` or
|
||
`WindowId` under `src/AcDream.App/UI` returns nothing.
|
||
|
||
**G5 — No per-window filter state.** The 64-bit `m_llTextTypeFilter` model and
|
||
the `PostInit` seeded defaults are decoded (colour-table doc §4) but unbuilt.
|
||
`ChatWindowController` renders `vm.RecentLinesDetailed()` unfiltered
|
||
(`ChatWindowController.cs:477`).
|
||
|
||
**G6 — No gameplay-options wire.** `PlayerDescriptionParser.cs:433-443` slices
|
||
the inbound `GameplayOptions` blob **heuristically** (`TryHeuristicInventoryStart`)
|
||
and never parses it; `SocialActions.cs:43-49` records that the outbound
|
||
`SetCharacterOptions (0x01A1)` full-blob builder was **deleted** in CH3 as
|
||
malformed and callerless. So there is currently no way to read or write
|
||
`0x1000008B`/`0x1000008C`.
|
||
|
||
**G7 — Local persistence has no opacity or window-id dimension.**
|
||
`RetailWindowLayoutPersistence.Capture` (`src/AcDream.App/UI/RetailWindowLayoutPersistence.cs:166-178`)
|
||
stores X/Y/W/H/Visible/Collapsed/Maximized only. It would persist four chat
|
||
windows correctly the moment they are registered under distinct `WindowNames`,
|
||
but it carries no opacity field and no filter field.
|
||
|
||
---
|
||
|
||
## 6. Recommended CH6 port shape
|
||
|
||
The single most valuable move is **G1**: import the layouts retail actually
|
||
uses. `0x2100006F` brings the eight resize grips, the 1/2/3/4 indicator buttons
|
||
and a coherent 410 × 100 root; `0x2100005B` is the floaty template. That
|
||
retires the hand-cropping, the dropped resize bar and the 9px patch in one
|
||
change, and it turns G2 from "add a resize model" into "import the one retail
|
||
authored".
|
||
|
||
### 6.1 Layering (matches the J4.1 communication-state pattern)
|
||
|
||
**Runtime — `RuntimeCommunicationState` extension** (it already owns the
|
||
transcript, reply/retell targets, rooms, friends and squelch, per
|
||
`docs/research/2026-07-26-slice-j4-1-communication-state.md`). Add a
|
||
presentation-free `ChatWindows` child owning, for ids 0..4:
|
||
- `ulong TextTypeFilter` per window, seeded by retail's `PostInit` switch
|
||
- `bool Open` per window (id 0 always open)
|
||
- `float DefaultOpacity` / `float ActiveOpacity` — **global**, not per-window,
|
||
with retail's `active >= default` coupling enforced in the setter pair
|
||
(`SetDefaultOpacity`/`SetActiveOpacity` semantics, §3)
|
||
- the routing predicate: display iff `windowId == m_eWindowID` **or**
|
||
(`windowId == 0` && `TypeIsActive(type)`) — the rule already decoded in the
|
||
colour-table doc §4, currently living nowhere.
|
||
|
||
Runtime owns this because no-window headless hosts already consume chat and
|
||
must not depend on presentation, and because the filter decides *routing*, not
|
||
appearance.
|
||
|
||
**Presentation — one `ChatWindowController` INSTANCE PER WINDOW.** The class is
|
||
already instance-based with no statics; give it a `WindowId` and a
|
||
`layoutId`/root-id pair so the same type binds both `0x2100006F` (main: adds
|
||
talk-focus menu, max/min, four indicator buttons) and `0x2100005B` (floaty:
|
||
adds title bar + close button). Register the four floaties as
|
||
`WindowNames.ChatWindow1..4` so `RetailWindowLayoutPersistence` picks them up
|
||
for free (G7).
|
||
|
||
**Input.** Four new actions `ToggleFloatingChatWindow1..4` in
|
||
`AcDream.UI.Abstractions/Input/`, defaulted to retail's modifier+digit chords
|
||
(§1.3), dispatched through the existing `InputDispatcher`. The four indicator
|
||
buttons call the same command; their pressed/normal state mirrors window
|
||
visibility exactly as `gmMainChatUI::RecvNotice_SetPanelVisibility` does.
|
||
|
||
**Settings.** Opacity becomes two `SettingsStore` floats plus two linked
|
||
sliders, matching `gmChatOptionsUI::InitOptions`'s `DualHash` linkage. Defer the
|
||
`0x1000008B`/`0x1000008C` wire (G6) — local persistence first, and file a
|
||
follow-up issue for the blob, because a malformed `0x01A1` is what CH3 already
|
||
had to delete once.
|
||
|
||
### 6.2 Slice sizing
|
||
|
||
| work | size | notes |
|
||
|---|---|---|
|
||
| **CH6a** — re-import main chat from `0x2100006F`; import + honour the eight Resizebar grips (element type 9, props `0x2A`–`0x2D`) in `LayoutImporter`; retire the 490px crop, the dropped resize bar and the 9px patch | **one slice**, but the biggest one | Fixes user reports (2) and (3)-artifacts. Visual gate required. |
|
||
| **CH6b** — make `Opacity` real: consume `UiRenderContext.AlphaMod` in every sprite/rect/text draw; delete the two hard-coded `(0,0,0,0.35)` tints | **one slice** | Fixes report (3)-transparency. Touches every widget draw path, so it wants its own slice and its own screenshot gate. Note the divergence: retail fades text too (§3); acdream's current comment at `UiRenderContext.cs:48-50` asserts the opposite as a deliberate choice — that row needs a decision and a register entry either way. |
|
||
| **CH6c** — Runtime `ChatWindows` state (filters + open flags + routing predicate) with no UI | **one slice** | Pure Runtime + tests, no visual gate. |
|
||
| **CH6d** — four floaty windows from `0x2100005B`, per-window controllers, toggle actions, indicator-button mirroring, close button, per-window local persistence | **one slice** | Depends on CH6a + CH6c. Fixes report (1). |
|
||
| **CH6e** — opacity settings UI + the two linked sliders | **small, deferrable** | |
|
||
| **CH6f** — `0x1000008B`/`0x1000008C` gameplay-options packing over `0x01A1` | **separate, later** | Do not bundle. CH3's deleted builder is the cautionary precedent. |
|
||
|
||
CH6a and CH6b both touch the render/import path and must not be parallelised
|
||
(`feedback_dont_parallelize_coupled_plan_slices`). CH6c is independent and can
|
||
run alongside either.
|
||
|
||
### 6.3 Divergence-register rows this work implies
|
||
|
||
- **Retires** any row asserting "chat window is not resizable from the top" once
|
||
CH6a lands.
|
||
- **New row** if CH6b keeps text out of the window alpha (retail's
|
||
`ChatInterface::SetOpacity @0x004F3120` fades the whole surface).
|
||
- **New row** for local-only chat-window persistence until CH6f, since retail
|
||
stores this server-side in `GameplayOptions`.
|
||
|
||
---
|
||
|
||
## Appendix — quick id reference
|
||
|
||
| id | meaning |
|
||
|---|---|
|
||
| `0x1000007E` | LayoutDesc attribute: chat window id (`m_eWindowID`) |
|
||
| `0x1000007F` | per-window text-type filter (bitfield64) |
|
||
| `0x10000080` | global default (unfocused) opacity (float) |
|
||
| `0x10000081` | global active (focused) opacity (float) |
|
||
| `0x10000086` / `0x10000087` | per-window X / Y |
|
||
| `0x10000088` / `0x10000089` | per-window width / height |
|
||
| `0x1000008A` | per-window visible flag |
|
||
| `0x1000008B` | per-window option-bag element name |
|
||
| `0x1000008C` | the per-window option ARRAY (global gameplay option) |
|
||
| `0x1000008D` | per-window title (StringInfo) |
|
||
| `0x2A` / `0x2B` / `0x2C` / `0x2D` | Resizebar bottom / left / right / top bools |
|
||
| `0x24` | LayoutDesc property: input action that toggles this element |
|
||
| `0x3C` / `0x3D` / `0x3E` / `0x3F` | max height / max width / min height / min width |
|
||
| `0x10000040` | element class `gmFloatyChatUI` |
|
||
| `0x10000041` | element class `gmMainChatUI` |
|
||
| `0x10000050` | element class `gmFloatyMainChatUI` |
|
||
| `9` | element class `UIElement_Resizebar` |
|
||
| `0x2100006F` | LayoutDesc — main chat window |
|
||
| `0x2100005B` | LayoutDesc — floating chat window (all four) |
|
||
| `0x10000601` | main chat window element (`<CHAT>`) |
|
||
| `0x10000505` / `0x1000050E` / `0x1000050F` / `0x10000510` | floating chat windows 1–4 (`<FCH1>`–`<FCH4>`) |
|
||
| `0x10000522`–`0x10000525` | main-window indicator buttons for windows 1–4 |
|
||
| `0x1000052A` | floaty-window close button |
|
||
| `0x100004D9` | floaty-window title bar |
|
||
| `0x1000046F` | main-window max/min button |
|