docs: CH6 research landed — chat-window shell plan (CH6a/b/c)
Wrong-LayoutDesc root cause (0x21000006 vs retail 0x2100006F), resident floating windows toggled by keybind, 8 authored resize grips, global opacity options 0x10000080/81. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
47e40900f3
commit
81f53bfd46
2 changed files with 746 additions and 3 deletions
|
|
@ -107,9 +107,36 @@ implementer per slice against a pinned contract (per
|
||||||
- **CH5 — closeout.** Register sweep, ledger flip, ISSUES updates,
|
- **CH5 — closeout.** Register sweep, ledger flip, ISSUES updates,
|
||||||
in-client test script for the user gate.
|
in-client test script for the user gate.
|
||||||
- **CH6 — chat-window shell parity (filed 2026-08-09 at user gate round
|
- **CH6 — chat-window shell parity (filed 2026-08-09 at user gate round
|
||||||
1).** Retail multi-window chat, per-window type filters + PostInit
|
1; research complete:
|
||||||
defaults (see color research §4), all-corner resize, opacity; research
|
`docs/research/2026-08-09-chat-retail-window-shell.md`).** Three
|
||||||
first.
|
sub-slices:
|
||||||
|
- **CH6a — correct main-window import + resize.** Swap the wrong
|
||||||
|
LayoutDesc `0x21000006` for retail's `0x2100006F` and delete the
|
||||||
|
downstream compensations (hand-cropped 490px ContentWidth, dropped
|
||||||
|
800px resize bar, the 9px transcript patch, orphan-sibling pruning);
|
||||||
|
teach `LayoutImporter` element type 9 (`UIElement_Resizebar`, 8
|
||||||
|
authored grips: 4 edges + 4 corners via bools 0x2A–0x2D); fix the
|
||||||
|
resize mask that excludes Top. Expected to also clear the round-1
|
||||||
|
artifact report.
|
||||||
|
- **CH6b — floating windows 1–4.** Mount `0x2100005B` ×4 as
|
||||||
|
always-resident children per `gmGamePlayUI::SetupChildren
|
||||||
|
@0x004E9EC0` (ids 0x10000505/0x1000050E/0x1000050F/0x10000510);
|
||||||
|
keybind actions `ToggleFloatingChatWindow1..4` (retail defaults at
|
||||||
|
`retail-default.keymap.txt:150-153`) through the dispatcher; the
|
||||||
|
main window's 1/2/3/4 buttons are one-directional state mirrors
|
||||||
|
(`gmMainChatUI::RecvNotice_SetPanelVisibility @0x004CCD80`), not
|
||||||
|
the toggle; per-window PostInit filter defaults (color research §4)
|
||||||
|
with the `windowId == m_eWindowID OR (windowId==0 && TypeIsActive)`
|
||||||
|
display rule.
|
||||||
|
- **CH6c — opacity.** Implement `UiRenderContext.AlphaMod`
|
||||||
|
consumption (whole-composited-window alpha per
|
||||||
|
`ChatInterface::SetOpacity @0x004F3120`); the two GLOBAL retail
|
||||||
|
options `0x10000080` (unfocused) / `0x10000081` (focused,
|
||||||
|
active >= default); retail defaults now, the user-facing Settings
|
||||||
|
slider in the settings track. Persistence: local settings first —
|
||||||
|
the retail per-window option array `0x1000008C` is stored by ACE as
|
||||||
|
an opaque byte[] it never parses, so the wire format is its own
|
||||||
|
deferred slice.
|
||||||
|
|
||||||
## Gates
|
## Gates
|
||||||
|
|
||||||
|
|
|
||||||
716
docs/research/2026-08-09-chat-retail-window-shell.md
Normal file
716
docs/research/2026-08-09-chat-retail-window-shell.md
Normal file
|
|
@ -0,0 +1,716 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
### 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 — 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 |
|
||||||
Loading…
Add table
Add a link
Reference in a new issue