using AcDream.Core.Chat; namespace AcDream.Core.Tests.Chat; /// /// Campaign CH slice CH6b: retail per-window text-type filter + open state /// (ChatInterface::PostInit @0x004F3DD0 defaults, color-table research /// doc §4, and RecvNotice_DisplayFinalStringInfo @0x004F4640's display /// predicate). /// public sealed class ChatWindowStateTests { // ── Defaults (research doc §4's table, verbatim) ──────────────────────── [Theory] [InlineData(0, 0xFBFFFFFFu)] [InlineData(1, 0x0000101Cu)] [InlineData(2, 0x00040C00u)] [InlineData(3, 0x00080000u)] [InlineData(4, 0x78000000u)] public void Defaults_MatchRetailPostInitTable(int windowId, ulong expectedFilter) { var state = new ChatWindowState(); Assert.Equal(expectedFilter, state.GetFilter(windowId)); } [Fact] public void Defaults_MainWindowIsAlwaysOpen_FloatingWindowsStartClosed() { var state = new ChatWindowState(); Assert.True(state.IsOpen(0)); for (int windowId = 1; windowId <= 4; windowId++) Assert.False(state.IsOpen(windowId)); } [Fact] public void Window1_DefaultFilter_MatchesSpeechTellDirectSendEmote() { var state = new ChatWindowState(); Assert.True(state.TypeIsActive(1, 0x02u)); // Speech Assert.True(state.TypeIsActive(1, 0x03u)); // Tell Assert.True(state.TypeIsActive(1, 0x04u)); // Speech_Direct_Send Assert.True(state.TypeIsActive(1, 0x0Cu)); // Emote Assert.False(state.TypeIsActive(1, 0x0Au)); // Social — not in window 1's default } [Fact] public void Window2_DefaultFilter_MatchesSocialSocialSendAllegiance() { var state = new ChatWindowState(); Assert.True(state.TypeIsActive(2, 0x0Au)); // Social Assert.True(state.TypeIsActive(2, 0x0Bu)); // Social_Send Assert.True(state.TypeIsActive(2, 0x12u)); // Allegiance Assert.False(state.TypeIsActive(2, 0x13u)); // Fellowship } [Fact] public void Window3_DefaultFilter_MatchesFellowshipOnly() { var state = new ChatWindowState(); Assert.True(state.TypeIsActive(3, 0x13u)); // Fellowship Assert.False(state.TypeIsActive(3, 0x0Au)); // Social Assert.False(state.TypeIsActive(3, 0x02u)); // Speech } [Fact] public void Window4_DefaultFilter_MatchesTurbineGeneralTradeLfgRoleplay() { var state = new ChatWindowState(); Assert.True(state.TypeIsActive(4, 0x1Bu)); // TurbineGeneral Assert.True(state.TypeIsActive(4, 0x1Cu)); // TurbineTrade Assert.True(state.TypeIsActive(4, 0x1Du)); // TurbineLFG Assert.True(state.TypeIsActive(4, 0x1Eu)); // TurbineRoleplay Assert.False(state.TypeIsActive(4, 0x20u)); // TurbineSociety — opt-in only Assert.False(state.TypeIsActive(4, 0x12u)); // Allegiance } [Fact] public void EveryWindow_NeverActivatesSocietyOrReservedByDefault() { var state = new ChatWindowState(); for (int windowId = 0; windowId <= 4; windowId++) { Assert.False(state.TypeIsActive(windowId, 0x20u)); // Society Assert.False(state.TypeIsActive(windowId, 0x21u)); // Reserved } } // ── TypeIsActive edge cases ────────────────────────────────────────────── [Fact] public void TypeIsActive_TypeAtOrAbove64_IsNeverActive() { var state = new ChatWindowState(); state.SetFilter(1, ulong.MaxValue); Assert.False(state.TypeIsActive(1, 64u)); Assert.False(state.TypeIsActive(1, 1000u)); } // ── Display rule matrix (windowId-addressed vs broadcast × filter hit/miss) ── [Fact] public void ShouldDisplay_ExplicitlyAddressed_AlwaysShowsRegardlessOfFilter() { var state = new ChatWindowState(); // Window 3's default filter has ONLY Fellowship (0x13) active — Speech // (0x02) would fail the broadcast check, but explicit addressing wins. Assert.True(state.ShouldDisplay(windowId: 3, targetWindowId: 3u, logTextType: 0x02u)); } [Fact] public void ShouldDisplay_ExplicitlyAddressedToAnotherWindow_NeverShowsHereEvenOnBroadcastFilterHit() { var state = new ChatWindowState(); // Addressed to window 2, evaluated from window 1's perspective: not a // broadcast (targetWindowId is a real window id, not // BroadcastTargetWindow) and not addressed to window 1. Assert.False(state.ShouldDisplay(windowId: 1, targetWindowId: 2u, logTextType: 0x02u)); } [Fact] public void ShouldDisplay_Broadcast_FilterHit_Shows() { var state = new ChatWindowState(); Assert.True(state.ShouldDisplay( windowId: 1, targetWindowId: ChatWindowState.BroadcastTargetWindow, logTextType: 0x02u)); // Speech } [Fact] public void ShouldDisplay_Broadcast_FilterMiss_DoesNotShow() { var state = new ChatWindowState(); Assert.False(state.ShouldDisplay( windowId: 1, targetWindowId: ChatWindowState.BroadcastTargetWindow, logTextType: 0x0Au)); // Social } [Fact] public void ShouldDisplay_MainWindow_BroadcastRespectsItsOwnFilter() { // CH6a/b REJECT-review BLOCKER (SHOULD-FIX 2): before the fix, window // 0's explicit-addressing branch (targetWindowId == windowId) used the // SAME literal 0 as the broadcast sentinel, so it short-circuited true // for EVERY broadcast line regardless of window 0's own filter. Now // that BroadcastTargetWindow is a distinct sentinel, the main // window's filter (0xFBFFFFFF — excludes 0x1A, Society opt-in) is // genuinely consulted for a broadcast line, exactly like every other // window. var state = new ChatWindowState(); Assert.False(state.ShouldDisplay( windowId: 0, targetWindowId: ChatWindowState.BroadcastTargetWindow, logTextType: 0x1Au)); // excluded Assert.False(state.ShouldDisplay( windowId: 0, targetWindowId: ChatWindowState.BroadcastTargetWindow, logTextType: 0x20u)); // Society, opt-in Assert.True(state.ShouldDisplay( windowId: 0, targetWindowId: ChatWindowState.BroadcastTargetWindow, logTextType: 0x02u)); // Speech } [Fact] public void ShouldDisplay_MainWindow_ExplicitlyAddressed_AlwaysShows() { // Window id 0 (main) is still a valid EXPLICIT address, distinct from // BroadcastTargetWindow — this is what keeps a future // targetWindowId == MainWindowId (AP-180's per-window echo) // expressible: explicit addressing to main always wins, even for a // type main's own filter would otherwise reject. var state = new ChatWindowState(); Assert.True(state.ShouldDisplay(windowId: 0, targetWindowId: 0u, logTextType: 0x1Au)); } // ── SetFilter / SetOpen / Toggle ───────────────────────────────────────── [Fact] public void SetFilter_MainWindow_Persists() { // CH6a/b REJECT-review SHOULD-FIX 2: dropped the old no-op — the main // window's filter is now genuinely settable, matching retail's own // options-page-driven main-window filter. var state = new ChatWindowState(); state.SetFilter(0, 0x1u); Assert.Equal(0x1u, state.GetFilter(0)); Assert.True(state.TypeIsActive(0, 0x00u)); Assert.False(state.TypeIsActive(0, 0x02u)); } [Fact] public void SetFilter_FloatingWindow_Persists() { var state = new ChatWindowState(); state.SetFilter(2, 0x1u); Assert.Equal(0x1u, state.GetFilter(2)); Assert.True(state.TypeIsActive(2, 0x00u)); } [Fact] public void SetOpen_MainWindow_IsANoOp_AlwaysOpen() { var state = new ChatWindowState(); state.SetOpen(0, false); Assert.True(state.IsOpen(0)); } [Fact] public void Toggle_FloatingWindow_FlipsOpenState_AndReturnsNewValue() { var state = new ChatWindowState(); Assert.False(state.IsOpen(1)); bool afterFirst = state.Toggle(1); Assert.True(afterFirst); Assert.True(state.IsOpen(1)); bool afterSecond = state.Toggle(1); Assert.False(afterSecond); Assert.False(state.IsOpen(1)); } [Fact] public void Toggle_MainWindow_AlwaysReturnsTrue_NeverCloses() { var state = new ChatWindowState(); bool result = state.Toggle(0); Assert.True(result); Assert.True(state.IsOpen(0)); } [Fact] public void ResetToDefaults_RestoresSeededFiltersAndOpenState() { var state = new ChatWindowState(); state.SetFilter(1, 0u); state.SetOpen(1, true); state.ResetToDefaults(); Assert.Equal(0x0000101Cu, state.GetFilter(1)); Assert.False(state.IsOpen(1)); } // ── Argument validation ─────────────────────────────────────────────── [Theory] [InlineData(-1)] [InlineData(5)] public void OutOfRangeWindowId_Throws(int windowId) { var state = new ChatWindowState(); Assert.Throws(() => state.GetFilter(windowId)); Assert.Throws(() => state.IsOpen(windowId)); Assert.Throws(() => state.TypeIsActive(windowId, 0u)); } // ── Revision counter ───────────────────────────────────────────────── [Fact] public void Revision_AdvancesOnFilterAndOpenChange_NotOnNoOpWrites() { var state = new ChatWindowState(); long baseline = state.Revision; state.SetFilter(1, 0x1u); Assert.True(state.Revision > baseline); long afterFilter = state.Revision; // No-op: same value. state.SetFilter(1, 0x1u); Assert.Equal(afterFilter, state.Revision); state.SetOpen(1, true); Assert.True(state.Revision > afterFilter); } }