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
65
src/AcDream.App/UI/Layout/ChatTranscriptRenderer.cs
Normal file
65
src/AcDream.App/UI/Layout/ChatTranscriptRenderer.cs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.UI.Abstractions.Panels.Chat;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Shared word-wrap + retail color-carry-forward transcript line builder.
|
||||
/// Factored out of <see cref="ChatWindowController.GetTranscriptLines"/>
|
||||
/// (Campaign CH slice CH6b) so <see cref="FloatingChatWindowController"/>
|
||||
/// reuses the exact same algorithm instead of duplicating it — both the main
|
||||
/// chat window and the four floating windows are views over the SAME
|
||||
/// <see cref="ChatVM"/> transcript (J4.1 pattern: one canonical log, many
|
||||
/// filtered presentations).
|
||||
///
|
||||
/// <para>
|
||||
/// Pure function — callers own their own per-controller layout cache
|
||||
/// (revision/wrap-width/font keyed), matching the caching each controller
|
||||
/// already had before this extraction.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static class ChatTranscriptRenderer
|
||||
{
|
||||
/// <param name="detailed">Tail of the shared chat log, formatted with retail metadata.</param>
|
||||
/// <param name="maxW">Wrap width in pixels.</param>
|
||||
/// <param name="measure">Glyph-width measurer for the active font.</param>
|
||||
/// <param name="accept">
|
||||
/// Optional per-line filter — retail's <c>ChatInterface::TypeIsActive</c>
|
||||
/// (or the full <c>ShouldDisplay</c> predicate) for THIS window. Null
|
||||
/// accepts every line (the main window has no user filter — color-table
|
||||
/// research doc §4). A line that fails the filter is dropped from this
|
||||
/// window's view WITHOUT advancing the carried-forward color, matching
|
||||
/// retail's <c>m_curFontColor</c> only advancing for lines actually
|
||||
/// appended to THIS window's own scroll (<c>AppendStringInfoWithFont</c>
|
||||
/// only runs for displayed lines).
|
||||
/// </param>
|
||||
public static List<UiText.Line> BuildLines(
|
||||
IReadOnlyList<FormattedLine> detailed,
|
||||
float maxW,
|
||||
Func<string, float> measure,
|
||||
Func<uint, bool>? accept)
|
||||
{
|
||||
var result = new List<UiText.Line>(detailed.Count);
|
||||
if (detailed.Count == 0)
|
||||
return result;
|
||||
|
||||
// Retail's font-color state (m_curFontColor) persists across every
|
||||
// line actually appended to this window — an out-of-range LogTextType
|
||||
// leaves it unchanged rather than reverting to a default (color-table
|
||||
// doc §3.2). Seed the carry with retail's own unfilled-slot default
|
||||
// (colorGreen, index 0x00).
|
||||
RetailChatColorTable.TryGetColor(0x00u, out Vector4 currentColor);
|
||||
foreach (FormattedLine d in detailed)
|
||||
{
|
||||
if (accept is not null && !accept(d.LogTextType))
|
||||
continue;
|
||||
if (RetailChatColorTable.TryGetColor(d.LogTextType, out Vector4 resolved))
|
||||
currentColor = resolved;
|
||||
foreach (string frag in ChatWindowController.WrapText(d.Text, maxW, measure))
|
||||
result.Add(new UiText.Line(frag, currentColor));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue