BLOCKER: ChatSettings.DefaultOpacity shipped retail's base ChatInterface value (0.5) as ONE shared global default applied to every RetailWindowManager-registered window, not just the four floating chat windows retail itself fades. That faded the whole out-of-box registered UI (radar, vitals, toolbar, main chat, ...) to 50% opacity, including several windows that can never take keyboard focus and so were stuck at 0.5 permanently. Fixed to gmMainChatUI's 1.0/1.0 override (0x004CD0F0) instead — retail-identical opaque presentation for the 11 non-chat windows and the main chat window; only the four floating chat windows now diverge from retail's 0.5-while-idle default, and the Settings -> Chat transparency slider remains fully user-settable. AP-190 reworded and gains two new decomp-verified clauses: (3) retail eases opacity toward its target by 5% of the delta per tick (ChatInterface::ListenToGlobalMessage @0x004F3840, armed from the focus element-messages at @0x004F5275) where acdream snaps -- deferred, needs a UI frame-tick hook the opacity controller doesn't have; (4) retail's focus predicate is the chat ENTRY FIELD specifically (ChatInterface::IsTextEntryFocused @0x004F30A0) where acdream uses any-focusable-descendant. Both findings + the pre-existing UiMenu.cs PushAlphaAbsolute(1f) popup bypass are folded into the window-shell research doc's opacity section. NITs: fixed the stale "text bypasses the alpha" comment in UiElement.DrawSelfAndChildren (CH6c already routed DrawStringDat/ DrawString through the same ApplyAlpha chokepoint as sprites/rects); added RetailWindowManager.WindowUnregistered + wired RetailWindowOpacityController to detach and forget a window unregistered while it held focus (previously only Dispose detached, leaking any window unregistered mid-focus for the rest of the session); added post-Dispose no-op guards to the three Set* opacity mutators; added a DrawString (BitmapFont path) alpha regression test and a DrawStringDat outline/background-pass alpha test (the existing tests only ever exercised the foreground/fill pass). Also fixes RuntimeSettingsControllerTests.SettingsViewModelSavePreserves SectionAndTargetOrder's now-stale "target-chat-opacity:0.5:1" expectation (caught by the full-suite run this fix requires) to match the new 1.0 default. Campaign ledger CH6c row updated to APPROVE-WITH-FIXES. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
47 KiB
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
§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.jsondocs/research/named-retail/retail-default.keymap.txtdocs/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 CTRL-digit quickslot rows —
corrected below; the original filing mislabeled 0x00000002 as "shift").
So retail's default is Alt+1 … Alt+4, not bare 1–4.
RESOLVED at Campaign CH slice CH6b: retail-default.keymap.txt's own
MetaKeys legend (not a guess — the keymap file's literal index table) reads
MetaKeys
[
1 [ 0 DIK_LSHIFT ]
2 [ 0 DIK_LCONTROL ]
2 [ 0 DIK_RCONTROL ]
3 [ 0 DIK_LMENU ]
3 [ 0 DIK_RALT ]
4 [ 0 DIK_LWIN ]
4 [ 0 DIK_RWIN ]
]
so the modifier-mask bit for MetaKeys index N is 1 << (N-1): index 1 =
Shift → 0x1, index 2 = Ctrl → 0x2, index 3 = Alt → 0x4, index 4 = Win →
0x8. 0x00000004 on the ToggleFloatingChatWindow1..4 rows is therefore
unambiguously Alt, cross-checked against the same file's own Alt+A/D
strafe and Alt+Enter/Tab/F4 rows (all 0x00000004). KeyBindings already
carried ModifierMask.Alt for these four actions since Phase K.1c — no code
change was needed, only removing this hedge.
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.
RESOLVED 2026-08-10 at Campaign CH slice CH6b; wording corrected at the
CH6a/b REJECT-review (NIT 4) — the original "ONLY function in the whole 2013
binary that branches on idMessage == 1" superlative was false
(gmFloatyChatUI::ListenToElementMessage @0x004CE330, the floaty windows'
close-button handler, also branches on idMessage == 1; §1.5). The
substantive claim stands: within gmMainChatUI::ListenToElementMessage
specifically, there is no case for 0x10000522-0x10000525. The prior
UNVERIFIED paragraph's hedge ("safe to wire both") is superseded by a direct
read of gmMainChatUI::ListenToElementMessage @0x004CDA80. It handles
exactly two element ids: 0x1000046f (max/min, dispatching
HandleMaximizeButton) and the talk-focus menu's selection message
(idMessage == 7, checked against this->m_pCCS / a 0x1000000b attribute
read). There is no case, anywhere in that function or its base-class fallback
(ChatInterface::ListenToElementMessage, called unconditionally at the
function's tail), for 0x10000522–0x10000525. Clicking a chat-window
indicator button does NOTHING in retail — reading (a), pure indicator, is
correct; reading (b) is refuted. The 0x24 input-action-property theory
in reading (b) does not even apply to a mouse click on the button itself: that
property only wires keyboard dispatch (UIElementManager:: DoVisibilityToggleAction in §1.3), not a button's own UIElement click
message, which routes to its LISTENING PARENT — and that parent's handler has
no case for these four ids. acdream ports this exactly: the four indicator
buttons (ChatWindowController._indicatorButtons) carry no OnClick at all;
ChatWindowController.SetIndicatorOpen is their only writer, called only from
RetailUiRuntime.OnWindowVisibilityChanged in response to the floating
window's own visibility changing (keybind or otherwise) — a pure one-directional
mirror, matching retail exactly.
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 @0x004CCE50looks up0x1000046F.gmMainChatUI::RecvNotice_SetPanelVisibility @0x004CCD80writes0x10000522–0x10000525.gmFloatyMainChatUI::PostInit @0x004D2670binds exactly the 16 ids0x10000693–0x100006A2, and casts eight of them viaDynamicCast(9)— element type 9 isUIElement_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.
RESOLVED at Campaign CH slice CH6c (2026-08-10) by static decomp, not cdb — the values are constructor-literal, so no live attach was needed. Retail's shipped defaults are PER WINDOW CLASS, not one constant:
004f4550 ChatInterface::ChatInterface(this, arg2, arg3) // BASE ctor
004f459f this->m_fDefaultOpacity = 0.5f;
004f45a5 this->m_fCurrentOpacity = 0.5f;
004f45ab this->m_fActiveOpacity = 1f;
004cd0f0 gmMainChatUI::gmMainChatUI(this, arg2, arg3) // derived, calls base first
004cd0ff ChatInterface::ChatInterface(this, arg2, arg3);
004cd148 this->m_fDefaultOpacity = 1f; // OVERRIDES base
004cd14e this->m_fCurrentOpacity = 1f;
// m_fActiveOpacity left at base's 1f
004ce2c0 gmFloatyChatUI::Create(arg1, arg2) // the 4 floating windows
004ce2e0 ChatInterface::ChatInterface(eax, arg1, arg2); // NO override — keeps base 0.5/1.0
gmFloatyMainChatUI (element class 0x10000050, the concrete class actually
instantiated for the retail main chat window — its DynamicCast accepts both
0x10000050 and 0x10000041) calls gmMainChatUI::gmMainChatUI as its own
base constructor (0x004D22B0) and adds no opacity override of its own, so it
inherits gmMainChatUI's 1.0/1.0.
So: the main chat window is ALWAYS FULLY OPAQUE in both states (Default=1.0,
Active=1.0) unless a saved GameplayOptions value overrides it via
UpdateFromPlayerModule; the four floating windows default to
Default=0.5/Active=1.0 (translucent when idle, opaque once the chat entry has
focus). These are IN-MEMORY CONSTRUCTED starting values for each window
INSTANCE's fields — they get overwritten the moment UpdateFromPlayerModule
successfully reads a persisted 0x10000080/0x10000081 value from
PlayerModule::InqOption (the SAME global option for every window instance),
which is why the two options are still correctly described as GLOBAL rather
than per-window: only a NEVER-SAVED option (a fresh character, nothing in the
GameplayOptions blob yet) lets the per-class constructed defaults show
through, and even then only until the user's first slider drag pushes one
shared value into every live window via RecvNotice_GameplayOptionChanged.
CH6c review-fix round (2026-08-10): the shared default above was WRONG.
Shipping the base ChatInterface value (0.5/1.0) as ONE shared global
default, combined with the scope extension to every registered window, faded
the WHOLE registered UI (radar, vitals, toolbar, main chat, ...) to 50%
opacity out of the box — including several windows that can never take
keyboard focus at all, so they were stuck at 0.5 permanently. acdream now
ships gmMainChatUI's per-class 1.0/1.0 override (0x004CD0F0) as the
shared global default instead (register row AP-190 in
docs/architecture/retail-divergence-register.md), which is retail-identical
for the 11 non-chat windows and the main chat window and leaves only the four
floating chat windows diverging from retail's 0.5-while-idle fade — a
default-VALUE divergence the transparency slider still fully covers. The
linking invariant (active >= default, restored by dragging the OTHER value —
verified from SetDefaultOpacity/SetActiveOpacity's own bodies, matching
the summary already recorded above) is ported exactly regardless of which
default seeds it.
3.1 Two more residuals found at the CH6c review (not yet ported)
Both are decomp-verified and both are recorded as new AP-190 clauses; neither is implemented this round.
(a) Retail eases opacity between endpoints; acdream snaps.
ChatInterface::ListenToGlobalMessage @0x004F3840 is the handler for global
message id 3, armed (UIListener::RegisterForGlobalMessage(this, 3)) from
the element-focus messages 0x1A/0x1E/0x28/0x29/0x2E inside the
window's ListenToElementMessage switch at 0x004F5275. Once armed, every
tick nudges the live opacity toward whichever endpoint
IsTextEntryFocused currently selects by 5% of the endpoint delta
(fabsl(target - current) * 0.05f), and unregisters from the global message
once the value lands within FP-epsilon of the target. acdream's
RetailWindowOpacityController.Apply sets the target opacity directly on the
focus-change event — the START and END states are retail-exact, but the
transition is an instant snap instead of a roughly 20-tick fade. Porting the
lerp needs a UI frame-tick hook RetailWindowOpacityController does not have
today (it only reacts to DescendantFocusChanged); deferred.
(b) Retail's focus predicate is the chat entry field specifically; acdream's
is any focusable descendant. ChatInterface::IsTextEntryFocused @0x004F30A0
tests GetFocusDescendant(rootElement) == this->m_chatEntry — literally the
window's text-entry element, not "some descendant of this window has focus."
acdream's RetailWindowHandle.DescendantFocusChanged (the event
RetailWindowOpacityController subscribes to) fires whenever ANY focusable
descendant of the window gains focus. For a window with exactly one
focusable child the two predicates coincide; for a window with several (a
settings panel's multiple controls, for example) acdream's broader predicate
holds ActiveOpacity while retail would already have faded back to
DefaultOpacity once focus left the specific text-entry widget.
Pre-existing, unrelated: UiMenu.cs:293's opacity bypass. Popup menus
call ctx.PushAlphaAbsolute(1f) before drawing so a menu always reads solid
even when it is opened from a translucent (faded) window — this is a
deliberate acdream-only presentation choice (menus must stay legible
regardless of the host window's current fade state), not a divergence from
either of the two opacity mechanisms documented in this section, and it
predates the CH6c slice.
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:
- 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. - Position/size restore is skipped entirely when
CPlayerSystem::GetPlayerSystem()->m_layoutFromFile != 0(004ce4a8,004d2a25) — i.e. aLoadScreenLayoutfile 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— setsCharacterOptionDataFlag.GameplayOptions(0x00000200) and writesCharacter.GameplayOptionsverbatim. - 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 callsSetCharacterGameplayOptions(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
PropertyCollectionpacking 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
SettingsStorewindow-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:
:29LayoutId = 0x21000006,:33RootId = 0x1000000E:34ResizeBarId = 0x1000000F— dropped at:204-205:35-42transcript panel / transcript / track / input bar / menu / input / send / max-min ids:228,:237BackgroundColor = (0,0,0,0.35)on transcript and input:383-426ToggleMaximize— a faithful port ofgmMainChatUI::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. CLOSED at Campaign CH slice CH6c
(2026-08-10). UiRenderContext.ApplyAlpha already gated DrawRect/
DrawFill/DrawSprite before this slice (added back at 1da697ec, well
before CH6 — the "zero consumers" framing below described the PUBLIC
AlphaMod property specifically, not the private _alpha/ApplyAlpha pair
those three draws already used); the actual gap was narrower than originally
scoped: (a) DrawStringDat/DrawString still passed applyAlpha: false, so
TEXT stayed sharp over a translucent window against retail's whole-surface
SetOpacity semantics — CH6c fixed both; (b) nothing ever SET a window's
Opacity below its 1f default, since RetailUiRuntime.MountChat deliberately
left it at 1f pending this slice. CH6c added
RetailWindowOpacityController (src/AcDream.App/UI/RetailWindowOpacityController.cs),
which drives every RetailWindowManager-registered window's live Opacity
from keyboard-focus state and the two retail-linked Default/Active floats,
now exposed as a Settings → Chat tab transparency slider pair
(SettingsPanel.RenderChatTab). See the verified defaults + linking
behavior above (§3) and register row AP-190. The original paragraph below is
kept verbatim as the historical record of what CH6a/b actually shipped —
do not re-run this investigation.
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 TextTypeFilterper window, seeded by retail'sPostInitswitchbool Openper window (id 0 always open)float DefaultOpacity/float ActiveOpacity— global, not per-window, with retail'sactive >= defaultcoupling enforced in the setter pair (SetDefaultOpacity/SetActiveOpacitysemantics, §3)- the routing predicate: display iff
windowId == m_eWindowIDor (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.
- CLOSED at CH6c: text now respects window alpha —
DrawStringDat/DrawStringroute throughApplyAlphaexactly likeDrawSprite/DrawRect/DrawFill, matchingChatInterface::SetOpacity's whole-surface fade. No divergence row needed for this part. - New row AP-190 (CH6c): acdream applies the two opacity options to EVERY
RetailWindowManagerwindow (chat + floaties + vitals + toolbar + everything else), where retail's mechanism only ever runs fromChatInterface-derived windows; and ships ONE shared default (the baseChatInterfacector's 0.5/1.0) rather thangmMainChatUI's per-class 1.0/1.0 override for the main window specifically. - 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 |