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
|
|
@ -109,6 +109,24 @@ public class ChatWindowControllerTests
|
|||
Id = 0x1000046Fu, Type = 3, X = 474, Y = 0, Width = 16, Height = 16,
|
||||
};
|
||||
|
||||
// Type 1 -> UiButton (DatWidgetFactory.Create) so SetIndicatorOpen tests
|
||||
// have a real IUiDatStateful to assert against. Normal/Highlight state
|
||||
// MEDIA (not just a States entry) must be authored or TrySetRetailState
|
||||
// can't resolve either name to an ActiveState (UiButton.HasStateMedia) —
|
||||
// same fixture shape ToolbarControllerTests uses for its own
|
||||
// SetPanelOpen(Normal/Highlight) mirror test.
|
||||
ElementInfo MakeIndicator(uint id, float y)
|
||||
{
|
||||
var info = new ElementInfo { Id = id, Type = 1, X = 5, Y = y, Width = 16, Height = 16 };
|
||||
info.StateMedia["Normal"] = (0x1u, 1);
|
||||
info.StateMedia["Highlight"] = (0x2u, 1);
|
||||
return info;
|
||||
}
|
||||
var indicator1 = MakeIndicator(0x10000522u, 5);
|
||||
var indicator2 = MakeIndicator(0x10000523u, 22);
|
||||
var indicator3 = MakeIndicator(0x10000524u, 39);
|
||||
var indicator4 = MakeIndicator(0x10000525u, 56);
|
||||
|
||||
var root = new ElementInfo
|
||||
{
|
||||
Id = 0x10000600u, Type = 3, Width = 490, Height = 100,
|
||||
|
|
@ -116,6 +134,10 @@ public class ChatWindowControllerTests
|
|||
root.Children.Add(transcriptPanel);
|
||||
root.Children.Add(inputBar);
|
||||
root.Children.Add(maxMinNode);
|
||||
root.Children.Add(indicator1);
|
||||
root.Children.Add(indicator2);
|
||||
root.Children.Add(indicator3);
|
||||
root.Children.Add(indicator4);
|
||||
|
||||
var layout = LayoutImporter.Build(root, NoTex, null);
|
||||
var vm = new ChatVM(log ?? new ChatLog());
|
||||
|
|
@ -487,4 +509,65 @@ public class ChatWindowControllerTests
|
|||
|
||||
Assert.Equal(new[] { "first", "", "third" }, lines);
|
||||
}
|
||||
|
||||
// ── SetIndicatorOpen: Campaign CH slice CH6b — the button mirror ─────────
|
||||
// gmMainChatUI::RecvNotice_SetPanelVisibility @0x004CCD80: State 6
|
||||
// (Highlight) when the floating window is visible, State 1 (Normal)
|
||||
// when it is not.
|
||||
|
||||
[Theory]
|
||||
[InlineData(1, 0x10000522u)]
|
||||
[InlineData(2, 0x10000523u)]
|
||||
[InlineData(3, 0x10000524u)]
|
||||
[InlineData(4, 0x10000525u)]
|
||||
public void SetIndicatorOpen_Open_SetsHighlightState(int windowId, uint indicatorId)
|
||||
{
|
||||
var (rootInfo, layout, vm) = BuildTestTree();
|
||||
var bus = new CaptureBus();
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, null, null, NoTex)!;
|
||||
var indicator = Assert.IsType<UiButton>(layout.FindElement(indicatorId));
|
||||
|
||||
ctrl.SetIndicatorOpen(windowId, open: true);
|
||||
|
||||
Assert.Equal(UiButtonStateMachine.Highlight, indicator.ActiveRetailStateId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetIndicatorOpen_Closed_SetsNormalState()
|
||||
{
|
||||
var (rootInfo, layout, vm) = BuildTestTree();
|
||||
var bus = new CaptureBus();
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, null, null, NoTex)!;
|
||||
var indicator = Assert.IsType<UiButton>(layout.FindElement(0x10000522u));
|
||||
|
||||
ctrl.SetIndicatorOpen(1, open: true);
|
||||
ctrl.SetIndicatorOpen(1, open: false);
|
||||
|
||||
Assert.Equal(UiButtonStateMachine.Normal, indicator.ActiveRetailStateId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetIndicatorOpen_DoesNotAffectOtherWindowsIndicators()
|
||||
{
|
||||
var (rootInfo, layout, vm) = BuildTestTree();
|
||||
var bus = new CaptureBus();
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, null, null, NoTex)!;
|
||||
var indicator2 = Assert.IsType<UiButton>(layout.FindElement(0x10000523u));
|
||||
|
||||
ctrl.SetIndicatorOpen(1, open: true);
|
||||
|
||||
Assert.Equal(UiButtonStateMachine.Normal, indicator2.ActiveRetailStateId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(5)]
|
||||
public void SetIndicatorOpen_OutOfRangeWindowId_Throws(int windowId)
|
||||
{
|
||||
var (rootInfo, layout, vm) = BuildTestTree();
|
||||
var bus = new CaptureBus();
|
||||
var ctrl = ChatWindowController.Bind(rootInfo, layout, vm, () => bus, null, null, NoTex)!;
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => ctrl.SetIndicatorOpen(windowId, open: true));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue