feat(chat): Campaign CH slice CH6b — floating chat windows 1-4
Mounts retail's four floating chat windows as always-resident, born-hidden children per gmGamePlayUI::SetupChildren @0x004E9EC0, all sharing LayoutDesc 0x2100005B (window ids 0x10000505/0x1000050E/0x1000050F/0x10000510). New FloatingChatWindowController (AcDream.App/UI/Layout) binds each window's own widget tree — built fresh per instance from one shared imported ElementInfo — reusing ChatWindowController's word-wrap + retail color-carry algorithm via the extracted ChatTranscriptRenderer instead of duplicating it. A floaty window has no talk-focus menu (research doc §2.2), so its entry field always sends on Say; the mismatch against retail's possible shared-channel behavior is UNVERIFIED and filed as #369/AP-188. Runtime owns the per-window filter/open state: ChatWindowState (new, AcDream.Core.Chat) seeds retail's exact PostInit defaults per window (window 1 0x0000101C Speech/Tell/DirectSend/Emote, window 2 0x00040C00 Social/SocialSend/Allegiance, window 3 0x00080000 Fellowship, window 4 0x78000000 Turbine General/Trade/LFG/Roleplay) and implements the full ShouldDisplay(windowId, targetWindowId, logTextType) display predicate from ChatInterface::RecvNotice_DisplayFinalStringInfo @0x004F4640. It lives on RuntimeCommunicationState.ChatWindows so every host borrows the same instance. The main window's filter (0xFBFFFFFF, "no user filter") never actually gates anything because its own explicit-address branch already covers every broadcast line — that's why UpdateFromPlayerModule early-returns for window 0 in retail, ported here by construction rather than a special case. Keybind wiring: InputAction.ToggleFloatingChatWindow1..4 and their KeyBindings.RetailDefaults() chords already existed since Phase K.1c (unwired until now). The MetaKeys table confirms retail's default is Alt+1 through Alt+4 (index 3 = bit 0x00000004, cross-checked against the same file's Alt+A/D strafe and Alt+Enter/Tab/F4 rows). Routes through GameplayInputCommandController -> RetainedGameplayWindowCommands -> RetailUiRuntime.ToggleFloatingChatWindow -> the generic UiHost.ToggleWindow, whose visibility-change event is the single chokepoint that syncs ChatWindowState.SetOpen and mirrors the main window's 1-4 indicator button regardless of what changed a window's visibility (keybind, close button, or a restored layout). A direct decomp read of gmMainChatUI::ListenToElementMessage @0x004CDA80 — the only function in the whole binary that branches on a click message — settles what the research doc had left as a hedge: it handles exactly 0x1000046f (max/min) and the talk-focus menu's selection message, with NO case for 0x10000522-0x10000525. The four indicator buttons are PURE one-directional mirrors in retail; clicking them does nothing. ChatWindowController.SetIndicatorOpen ports this with no OnClick at all. Corrected research doc §1.4 accordingly. Persistence is local-only (register row AP-187; the retail 0x1000008C GameplayOptions wire remains deferred to CH6f): window geometry and open/visible state ride the existing generic RetailWindowLayoutPersistence path for free once each window registers under its own WindowNames entry; the four filter masks get a dedicated ChatSettings round-trip (ChatWindow1Filter..ChatWindow4Filter, defaulting to the retail PostInit constants) loaded at mount and saved alongside SaveLayout(). Tests: ChatWindowStateTests (defaults, TypeIsActive, the full display-rule matrix, toggle/reset, revision counter), FloatingChatWindowControllerTests (bind smoke tests against a synthetic 0x2100005B tree, per-window filter routing, filter-change cache invalidation, fixed-Say submit), new ChatWindowController.SetIndicatorOpen tests (Highlight/Normal state, cross-window isolation, range validation), GameplayInputCommandController routing for the four toggle actions, and a SettingsStore filter round-trip. Full Release suite: 12,392 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
41b408f3e6
commit
22020ef2c4
21 changed files with 1699 additions and 46 deletions
|
|
@ -62,9 +62,10 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
// Chat-window 1-4 state-mirror 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, not the reverse) and
|
||||
// the floating windows themselves are CH6b's scope — these import generically
|
||||
// (visible, inert) here and are left for CH6b to wire.
|
||||
// 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.
|
||||
private const uint Indicator1Id = 0x10000522u;
|
||||
private const uint Indicator2Id = 0x10000523u;
|
||||
private const uint Indicator3Id = 0x10000524u;
|
||||
|
|
@ -176,6 +177,11 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
private bool _maximized;
|
||||
private UiButton? _maxMinButton;
|
||||
|
||||
// Chat-window 1-4 indicator buttons, indexed [windowId - 1]. Resolved at
|
||||
// Bind() time (see the Indicator1Id..Indicator4Id class doc above);
|
||||
// CH6b wires SetIndicatorOpen as their only writer.
|
||||
private readonly UiButton?[] _indicatorButtons = new UiButton?[4];
|
||||
|
||||
// ── Factory ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -244,6 +250,14 @@ 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). ──
|
||||
uint[] indicatorIds = { Indicator1Id, Indicator2Id, Indicator3Id, Indicator4Id };
|
||||
for (int i = 0; i < indicatorIds.Length; i++)
|
||||
c._indicatorButtons[i] = layout.FindElement(indicatorIds[i]) as UiButton;
|
||||
|
||||
// ── Transcript ───────────────────────────────────────────────────
|
||||
// The factory now builds the Type-12 transcript element (0x10000011) as a UiText.
|
||||
// Find it in the widget tree and bind the live providers — no remove/add needed.
|
||||
|
|
@ -460,6 +474,35 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
handle.MoveTo(frame.Left, targetTop);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirror a floating chat window's (<paramref name="windowId"/> 1-4)
|
||||
/// open/closed state onto its main-window indicator button — the exact
|
||||
/// 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.
|
||||
///
|
||||
/// <para>
|
||||
/// One-directional by design — a decomp read of
|
||||
/// <c>gmMainChatUI::ListenToElementMessage @0x004CDA80</c> (the ONLY
|
||||
/// function in the whole 2013 binary that branches on
|
||||
/// <c>idMessage == 1</c>, i.e. "clicked") shows it 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).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public void SetIndicatorOpen(int windowId, bool open)
|
||||
{
|
||||
if (windowId < 1 || windowId > _indicatorButtons.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(windowId));
|
||||
_indicatorButtons[windowId - 1]?.TrySetRetailState(
|
||||
open ? UiButtonStateMachine.Highlight : UiButtonStateMachine.Normal);
|
||||
}
|
||||
|
||||
public RetainedWindowState CaptureWindowState()
|
||||
=> new(
|
||||
Maximized: _maximized,
|
||||
|
|
@ -524,27 +567,13 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
: debugFont is { } bf ? s => bf.MeasureWidth(s)
|
||||
: static s => s.Length * 7f;
|
||||
|
||||
// Retail's font-color state (m_curFontColor) persists across every
|
||||
// appended line — an out-of-range LogTextType leaves it unchanged
|
||||
// rather than reverting to a default (research doc §3.2). Seed the
|
||||
// carry with retail's own unfilled-slot default (colorGreen, index
|
||||
// 0x00) and fold forward across the transcript in order. This is an
|
||||
// approximation of retail's LayoutDesc-initialized m_curFontColor:
|
||||
// acdream re-seeds at 0x00 and restarts the fold every render
|
||||
// window rather than carrying one persistent field across the
|
||||
// window's whole lifetime. Unreachable in practice today — no
|
||||
// producer emits a LogTextType >= 0x22 (RetailChatColorTable.Colors
|
||||
// covers the full 0x00-0x21 retail index space), so the carry path
|
||||
// below never actually fires outside tests.
|
||||
RetailChatColorTable.TryGetColor(0x00u, out Vector4 currentColor);
|
||||
var result = new List<UiText.Line>(detailed.Count);
|
||||
foreach (var d in detailed)
|
||||
{
|
||||
if (RetailChatColorTable.TryGetColor(d.LogTextType, out Vector4 resolved))
|
||||
currentColor = resolved;
|
||||
foreach (var frag in WrapText(d.Text, maxW, measure))
|
||||
result.Add(new UiText.Line(frag, currentColor));
|
||||
}
|
||||
// Campaign CH slice CH6b: the wrap + retail color-carry-forward
|
||||
// algorithm is now shared with FloatingChatWindowController via
|
||||
// ChatTranscriptRenderer (approximation note on the color carry
|
||||
// moved there). The main window passes accept:null — it has no
|
||||
// user filter (color-table research doc §4); this is behaviorally
|
||||
// identical to the inline loop this replaced.
|
||||
var result = ChatTranscriptRenderer.BuildLines(detailed, maxW, measure, accept: null);
|
||||
return StoreTranscriptLayout(result, revision, maxW, datFont, debugFont);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue