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
|
|
@ -46,7 +46,22 @@ public sealed record ChatSettings(
|
|||
bool ShowTimestamps, // 0x40 — TimeStamp prefix on chat lines
|
||||
bool FilterProfanity, // 0x20000 — FilterLanguage (Turbine's profanity filter)
|
||||
// Visual / UX (no retail bitfield).
|
||||
float FontSize) // chat panel font, 10..20 pt
|
||||
float FontSize, // chat panel font, 10..20 pt
|
||||
// Campaign CH slice CH6b: local-only persistence of the four floating
|
||||
// chat windows' 64-bit text-type filters
|
||||
// (AcDream.Core.Chat.ChatWindowState — retail's per-window
|
||||
// 0x1000007F option, docs/research/2026-08-09-chat-retail-color-table.md
|
||||
// §4). Retail persists these server-side inside the opaque
|
||||
// 0x1000008C GameplayOptions blob (window-shell research doc §4.4);
|
||||
// acdream has no writer for that blob yet, so these fields are the
|
||||
// interim local store, with default values matching retail's own
|
||||
// ChatInterface::PostInit @0x004F3DD0 seed exactly. Trailing with
|
||||
// defaults so no existing positional/named ChatSettings construction
|
||||
// site needed to change.
|
||||
ulong ChatWindow1Filter = 0x0000101Cu, // Speech, Tell, Speech_Direct_Send, Emote
|
||||
ulong ChatWindow2Filter = 0x00040C00u, // Social, Social_Send, Allegiance
|
||||
ulong ChatWindow3Filter = 0x00080000u, // Fellowship
|
||||
ulong ChatWindow4Filter = 0x78000000u) // Turbine General/Trade/LFG/Roleplay
|
||||
{
|
||||
/// <summary>
|
||||
/// N4 (CH3 Opus review): matches ACE's ACTUAL
|
||||
|
|
|
|||
|
|
@ -195,7 +195,11 @@ public sealed class SettingsStore
|
|||
AppearOffline: ReadBool (chat, "appearOffline", d.AppearOffline),
|
||||
ShowTimestamps: ReadBool (chat, "showTimestamps", d.ShowTimestamps),
|
||||
FilterProfanity: ReadBool (chat, "filterProfanity", d.FilterProfanity),
|
||||
FontSize: ReadFloat(chat, "fontSize", d.FontSize));
|
||||
FontSize: ReadFloat(chat, "fontSize", d.FontSize),
|
||||
ChatWindow1Filter: ReadULong(chat, "chatWindow1Filter", d.ChatWindow1Filter),
|
||||
ChatWindow2Filter: ReadULong(chat, "chatWindow2Filter", d.ChatWindow2Filter),
|
||||
ChatWindow3Filter: ReadULong(chat, "chatWindow3Filter", d.ChatWindow3Filter),
|
||||
ChatWindow4Filter: ReadULong(chat, "chatWindow4Filter", d.ChatWindow4Filter));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -537,7 +541,11 @@ public sealed class SettingsStore
|
|||
private static SortedDictionary<string, object> BuildChatObject(ChatSettings c)
|
||||
=> new(StringComparer.Ordinal)
|
||||
{
|
||||
["appearOffline"] = c.AppearOffline,
|
||||
["appearOffline"] = c.AppearOffline,
|
||||
["chatWindow1Filter"] = c.ChatWindow1Filter,
|
||||
["chatWindow2Filter"] = c.ChatWindow2Filter,
|
||||
["chatWindow3Filter"] = c.ChatWindow3Filter,
|
||||
["chatWindow4Filter"] = c.ChatWindow4Filter,
|
||||
["filterProfanity"] = c.FilterProfanity,
|
||||
["fontSize"] = c.FontSize,
|
||||
["hearGeneralChat"] = c.HearGeneralChat,
|
||||
|
|
@ -655,6 +663,13 @@ public sealed class SettingsStore
|
|||
=> obj.TryGetProperty(name, out var el) && el.ValueKind == JsonValueKind.Number
|
||||
? el.GetSingle() : fallback;
|
||||
|
||||
private static ulong ReadULong(JsonElement obj, string name, ulong fallback)
|
||||
{
|
||||
if (!obj.TryGetProperty(name, out var el) || el.ValueKind != JsonValueKind.Number)
|
||||
return fallback;
|
||||
return el.TryGetUInt64(out ulong value) ? value : fallback;
|
||||
}
|
||||
|
||||
private static QualityPreset ReadQuality(JsonElement obj, string name, QualityPreset fallback)
|
||||
{
|
||||
if (!obj.TryGetProperty(name, out var el) || el.ValueKind != JsonValueKind.String)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue