fix(chat): round 4 — no user-visible meta text, real /help groups, indicator buttons toggle

Item 3 (#364): every honesty marker is now gone from user-visible /help
text. AllegianceOverview/HouseOverview's "[IMPLEMENTED]" tags and trailing
"Subcommands NOT marked..." sentences, and Day/Log/Render/Motd's appended
"NOT YET IMPLEMENTED in acdream" tails, are removed; the underlying retail
text is corrected/completed against the pseudo-C's own pristine
consolidated data dumps (Log and Motd had been silently truncated; Render
was entirely acdream-authored and is replaced with the real retail usage
string). The three PARTIAL /help group topics (channels/chatting/commands)
are now COMPLETE verbatim listings: HelpStupidChannelHack's three
"vtable slot" operands, previously believed undecodable, are the same
pooled/mislabeled-data artifact this campaign has hit before (AP-113's
precedent) — reading the function's own disassembly for the push imm32
preceding each constructor call resolves all three directly. messagetypes
is now a real ported construction (IsLegalChannel's 14-id whitelist +
LogTextTypeToString's name table + the exact join/wrap format) instead of
an acdream summary. Register row AP-184 retired.

Item 5: the main window's 1/2/3/4 indicator buttons now toggle their
floating chat window on click, per the user's retail memory overruling
the earlier decomp-only reading. UIElement_Button::HandleButtonClick has
its own generic click-driven action dispatch (property 0x12) reaching the
same DoVisibilityToggleAction the Alt+1..4 keybinds use; the button
fixture confirms this half is genuinely armed, but the floating-window
fixture authors no matching listener-registration property, so the
generic mechanism has no proven target in the data on hand. Per
CLAUDE.md, the user's retail memory is the axiom regardless:
ChatWindowController.BindIndicatorClicks wires each indicator's click
through the same ToggleFloatingChatWindow chokepoint the keybinds use,
as explicit user-directed retail behavior. SetIndicatorOpen stays the
sole writer of the Selected mirror so the visual stays consistent
through the click round trip.

Full reconciliation in docs/research/2026-08-09-chat-retail-window-shell.md
§1.4. Campaign plan gets the round-4 findings section; items 1+2
(text-style) are under parallel research, item 4 passed, item 6 deferred
to the settings track.

Suite: 12,579 passed / 4 skipped / 0 failed (Release, complete solution),
up from baseline 12,553/4/0 — net +26 tests, zero regressions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-10 18:46:34 +02:00
parent 8b166f3ea2
commit 5b54387b8e
10 changed files with 1129 additions and 239 deletions

View file

@ -59,13 +59,15 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
private const uint SendId = 0x10000019u;
private const uint MaxMinId = 0x1000046Fu;
// Chat-window 1-4 state-mirror indicator buttons
// Chat-window 1-4 indicator buttons
// (gmMainChatUI::RecvNotice_SetPanelVisibility @0x004CCD80): the button lights
// up iff the corresponding floating chat window is visible. The mechanism is
// one-directional (window visibility drives the button, never the reverse —
// see SetIndicatorOpen's doc for the decomp confirmation). Campaign CH slice
// CH6b resolves these to _indicatorButtons and wires them via SetIndicatorOpen,
// called by RetailUiRuntime whenever a floating chat window's visibility changes.
// up iff the corresponding floating chat window is visible. Campaign CH slice
// CH6b resolves these to _indicatorButtons and wires the MIRROR half via
// SetIndicatorOpen, called by RetailUiRuntime whenever a floating chat window's
// visibility changes. Round 4 (2026-08-10) adds the CLICK half — see
// SetIndicatorOpen's doc for the full reconciliation between the earlier
// decomp-only reading (no per-window code case) and the user's retail memory
// (clicking opens/closes the window) — both are correct, at different layers.
private const uint Indicator1Id = 0x10000522u;
private const uint Indicator2Id = 0x10000523u;
private const uint Indicator3Id = 0x10000524u;
@ -265,17 +267,21 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
if (layout.FindElement(id) is { } twin)
twin.Visible = false;
// ── Chat-window 1-4 indicator buttons — resolve now, wired later by
// RetailUiRuntime via SetIndicatorOpen as each floating window's own
// visibility changes (gmMainChatUI::RecvNotice_SetPanelVisibility
// @0x004CCD80 — see this class's doc + Indicator1Id..Indicator4Id).
// CH6a/b REJECT-review SHOULD-FIX 3: these carry DAT property 0x0B
// (ToggleBehavior) = true, but retail's own click dispatch
// (gmMainChatUI::ListenToElementMessage @0x004CDA80) has no case for
// any of their element ids — clicking one does NOTHING in retail.
// SuppressSelfToggle keeps SetIndicatorOpen the ONLY writer of their
// Selected mirror; without it, a click would flip the Highlight/Normal
// art with no underlying visibility change. ──
// ── Chat-window 1-4 indicator buttons — resolve now; the MIRROR half
// is wired later by RetailUiRuntime via SetIndicatorOpen as each
// floating window's own visibility changes
// (gmMainChatUI::RecvNotice_SetPanelVisibility @0x004CCD80 — see this
// class's doc + Indicator1Id..Indicator4Id); the CLICK half is wired
// by RetailUiRuntime calling BindIndicatorClicks after this method
// returns (round 4, 2026-08-10 — see SetIndicatorOpen's doc for the
// full reconciliation). SuppressSelfToggle stays true regardless:
// these carry DAT property 0x0B (ToggleBehavior) = true, but the
// button's own blind self-flip (UiButton.OnEvent's MouseUp case)
// would race the REAL toggle's outcome — SetIndicatorOpen (called
// synchronously inside the click, through RetailUiRuntime's
// WindowVisibilityChanged plumbing) stays the ONE authoritative
// writer of Selected, so a click's visual result always matches the
// window's actual new state instead of a guessed flip. ──
uint[] indicatorIds = { Indicator1Id, Indicator2Id, Indicator3Id, Indicator4Id };
for (int i = 0; i < indicatorIds.Length; i++)
{
@ -507,22 +513,81 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
/// state transition <c>gmMainChatUI::RecvNotice_SetPanelVisibility
/// @0x004CCD80</c> performs: <c>State 6</c> (Highlight/"lit") when the
/// floating window is visible, <c>State 1</c> (Normal) when it is not.
/// This half of the mechanism is genuinely one-directional in the exact
/// sense that <c>SetIndicatorOpen</c> is the ONLY writer of
/// <see cref="UiButton.Selected"/> — see <see cref="BindIndicatorClicks"/>
/// for the click half, which triggers the real toggle THROUGH this same
/// writer rather than flipping the indicator itself.
///
/// <para>
/// One-directional by design — a decomp read of
/// <c>gmMainChatUI::ListenToElementMessage @0x004CDA80</c> (one of
/// several functions in the 2013 binary that branch on
/// <c>idMessage == 1</c>, i.e. "clicked" — CH6a/b REJECT-review NIT 4
/// corrected the earlier "the ONLY function" superlative, e.g.
/// <c>gmFloatyChatUI::ListenToElementMessage @0x004CE330</c> also
/// branches on it for the floaty close button) shows THIS function
/// handles exactly two element ids: <c>0x1000046f</c> (max/min) and the
/// talk-focus menu's selection message. There is no case for
/// <c>0x10000522</c>-<c>0x10000525</c> — clicking a chat-window
/// indicator button does NOTHING in retail. acdream ports this exactly:
/// these buttons have no <c>OnClick</c> (research doc §1.4 corrected —
/// its own hedge that "it is safe to wire both" is a weaker reading than
/// this direct decomp confirmation, and is now superseded by it).
/// <b>Round 4 reconciliation (2026-08-10) — the user's retail memory
/// ("clicking opens the window") overruled CH6b's decomp-only reading
/// ("clicking does nothing"), and a deeper grep found the mechanism that
/// makes BOTH readings correct at their own layer.</b> CH6b's citation of
/// <c>gmMainChatUI::ListenToElementMessage @0x004CDA80</c> (no case for
/// <c>0x10000522</c>-<c>0x10000525</c>) is STILL TRUE as a statement
/// about that one function — but it was the wrong place to look for a
/// button click. Retail buttons don't route clicks through their
/// PARENT WINDOW's message handler at all: every <c>Type 1</c>
/// (<c>UIElement_Button</c>) button has its own generic click path,
/// <c>UIElement_Button::HandleButtonClick @0x00471E50</c>, which reads
/// an ENUM-kind DAT property <c>0x12</c> off ITSELF; when present, it
/// builds an <c>InputEvent</c> and routes it through
/// <c>ICIDM</c>/<c>UIElementManager</c>'s action map, which (for a
/// visibility action) reaches <c>UIElementManager::DoVisibilityToggleAction
/// @0x0045B660</c> — a lookup into
/// <c>m_elementInputActionListenerTable</c> (populated by
/// <c>UIElementManager::RegisterElementForInputAction</c>, itself called
/// from ONE place: <c>UIElement::Initialize</c>'s generic property switch,
/// case for ENUM-kind property <c>0x24</c> — ANY element authoring that
/// property registers itself as a listener for that action id) — for
/// every registered listener, <c>DoVisibilityToggleAction</c> broadcasts
/// element message <c>0x31</c>, which <c>UIElement::ListenToElementMessage</c>
/// (the ultimate base-class handler every window falls through to when
/// its own override, and <c>ChatInterface</c>'s, don't claim the
/// message — confirmed for BOTH <c>gmMainChatUI</c> and
/// <c>gmFloatyChatUI</c>) handles GENERICALLY: it reads the RECEIVING
/// element's OWN enum property <c>0x58</c> and, if it equals <c>1</c>
/// ("toggle"), calls <c>SetVisible(!currentlyVisible)</c>.
/// </para>
///
/// <para>
/// This is a real, complete, working "generic UI action" system — the
/// task's own hypothesis — and the fixture confirms the indicator
/// buttons genuinely author property <c>0x12</c> as an Enum (not merely
/// a stray/mistyped property): <c>chat_2100006f.json</c>'s four
/// indicator elements each carry it, with values
/// <c>0x10000514</c>-<c>0x10000517</c> in id order. But the OTHER half
/// of the wiring is where retail's own data falls short: the floating
/// chat window fixture (<c>chat_floaty_2100005b.json</c>) authors NO
/// Enum-kind property <c>0x24</c> anywhere (its one hit on property
/// number 36 is Integer-kind, an unrelated attribute) and NO property
/// <c>0x58</c> at all — so nothing in the shipped LayoutDesc data ever
/// registers a floating chat window as a listener for those four action
/// ids, and <c>RegisterElementForInputAction</c> has exactly one call
/// site in the whole binary (the property-driven one above; no class
/// anywhere calls it directly in code). <c>DoVisibilityToggleAction</c>
/// would find zero listeners and silently no-op. (The four action-id
/// VALUES themselves are not chat-specific either — the SAME four
/// numbers are <c>gmFriendsUI::PostInit</c>'s own Add/Remove/Tell
/// button and friends-listbox child ids, a coincidence of Turbine's
/// global asset-id allocator, not a cross-reference.) So even with the
/// generic mechanism confirmed real and armed on the button side, the
/// authored DATA available to us does not wire a target — which is
/// consistent with, not a refutation of, the original CH6b grep.
/// </para>
///
/// <para>
/// Per CLAUDE.md, the user's own retail memory is the axiom the code
/// must match regardless of what a specific grep or fixture shows.
/// <see cref="BindIndicatorClicks"/> makes each indicator's click drive
/// the SAME <c>ToggleFloatingChatWindow(windowId)</c> chokepoint the
/// <c>Alt+1..4</c> keybinds use — this is USER-DIRECTED retail behavior
/// (the generic action-dispatch mechanism exists and is plausibly HOW
/// retail wires it, but we cannot prove the exact target registration
/// from the data on hand), not a re-guess of the mirror-only reading.
/// See <c>docs/research/2026-08-09-chat-retail-window-shell.md</c> §1.4
/// for the full writeup.
/// </para>
/// </summary>
public void SetIndicatorOpen(int windowId, bool open)
@ -533,6 +598,29 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
open ? UiButtonStateMachine.Highlight : UiButtonStateMachine.Normal);
}
/// <summary>
/// Wire each indicator button's click to <paramref name="toggleFloatingWindow"/>
/// (RetailUiRuntime's <c>ToggleFloatingChatWindow</c>, the SAME chokepoint the
/// <c>Alt+1..4</c> keybinds use) — round 4 (2026-08-10), see
/// <see cref="SetIndicatorOpen"/>'s doc for the full reconciliation. Called by
/// <c>RetailUiRuntime</c> after <see cref="Bind"/> returns, once the runtime's own
/// toggle method is available. <see cref="UiButton.SuppressSelfToggle"/> stays true
/// on every indicator (set in <see cref="Bind"/>) so this click-triggered toggle's
/// outcome — not a blind self-flip — is what <see cref="SetIndicatorOpen"/> mirrors
/// back onto <see cref="UiButton.Selected"/>, keeping the visual state consistent
/// through the full click round trip even if the toggle is ever refused.
/// </summary>
public void BindIndicatorClicks(Func<int, bool> toggleFloatingWindow)
{
ArgumentNullException.ThrowIfNull(toggleFloatingWindow);
for (int i = 0; i < _indicatorButtons.Length; i++)
{
int windowId = i + 1;
if (_indicatorButtons[i] is { } indicator)
indicator.OnClick = () => toggleFloatingWindow(windowId);
}
}
public RetainedWindowState CaptureWindowState()
=> new(
Maximized: _maximized,