using System; using System.Threading; namespace AcDream.Core.Chat; /// /// Retail's per-chat-window text-type filter and open/visible state — window /// id 0 is the main chat window, ids 1-4 are the four /// floating chat windows (Campaign CH slice CH6b, /// docs/research/2026-08-09-chat-retail-window-shell.md §1.2 and /// docs/research/2026-08-09-chat-retail-color-table.md §4). These are /// acdream's OWN compact indices, not retail's raw m_eWindowID values: /// retail's actual main window is m_eWindowID == 8, its floaties are /// 2-5, and m_eWindowID == 0 is retail's ctor-default /// "unauthored" sentinel — none of that is 0-4 (CH6a/b /// REJECT-review SHOULD-FIX 2, /// docs/research/2026-08-10-ch6ab-review-findings.md). /// /// /// Ports two retail mechanisms exactly: /// /// ChatInterface::PostInit @0x004F3DD0's m_oldState /// switch seeds each window's default 64-bit /// m_llTextTypeFilter (color-table doc §4's table — the constants /// below are byte-identical to that table; retail's own main-window default /// 0xFBFFFFFF is shared by m_eWindowID 1 AND 8, and the floaty /// defaults come from retail m_eWindowID 2-5, one higher than /// acdream's compact 1-4). /// ChatInterface::RecvNotice_DisplayFinalStringInfo /// @0x004F4640's display predicate: a line shows in window /// W when the message's target window id equals W /// (explicit addressing) OR the message is broadcast /// () AND W's filter accepts the /// line's (ChatInterface::TypeIsActive /// @0x004F2F10). /// /// /// /// /// is a sentinel distinct from every real /// window id (0-4) — the acdream-internal analogue of retail's /// wire arg5 == 0 broadcast marker, deliberately kept separate from /// window id 0 (main). Earlier CH6b code conflated the two (both were /// literal 0), so 's explicit-addressing /// branch (targetWindowId == windowId) short-circuited true for EVERY /// broadcast line evaluated against the main window, regardless of its own /// filter — the CH6a/b REJECT-review BLOCKER. With the sentinel separated /// out, the main window's filter is genuinely consulted for broadcast lines, /// is a real (not inert) write for window 0 /// too, and a future explicit targetWindowId == MainWindowId (AP-180's /// m_idCurrentCommandSource per-window echo) stays distinguishable /// from an ordinary broadcast line. / /// keep their own, UNRELATED no-op for window 0 — retail's main window /// is simply never closable, independent of the broadcast-sentinel fix. /// /// /// /// No production carries an explicit target window /// id yet (register row AP-180 — the windowId dual-destination echo /// is deferred); every current line is effectively broadcast /// (targetWindowId == ). /// still accepts the full retail shape so the /// routing predicate does not need to change shape when AP-180 lands. /// /// public sealed class ChatWindowState { public const int MainWindowId = 0; public const int MinFloatingWindowId = 1; public const int MaxFloatingWindowId = 4; /// /// Retail's authored per-window text-type filter defaults /// (gmChatOptionsUI::InitOptions @0x0049FC60 / /// AddCheckboxBitfield64Option @0x0049EDA0's SetDefaultValue calls, /// docs/research/2026-08-10-options-panel-structure.md §5.2). Named here /// (rather than only as the private literals in ) /// because Campaign OP slice OP5's Chat-tab Defaults button needs the SAME /// values seeds — one source, not two independently /// literal tables that could drift. /// public const ulong MainWindowDefaultFilter = 0xFBFFFFFFul; public const ulong Floaty1DefaultFilter = 0x0000101Cul; public const ulong Floaty2DefaultFilter = 0x00040C00ul; public const ulong Floaty3DefaultFilter = 0x00080000ul; public const ulong Floaty4DefaultFilter = 0x78000000ul; /// /// Sentinel targetWindowId meaning "broadcast to every window, /// subject to each window's own filter" — the acdream-internal analogue /// of retail's wire arg5 == 0. Deliberately outside the /// 0-4 real-window-id range (see the class doc) so it can /// never collide with . /// public const uint BroadcastTargetWindow = uint.MaxValue; private const int WindowCount = MaxFloatingWindowId + 1; private readonly object _gate = new(); private readonly ulong[] _filters = new ulong[WindowCount]; private readonly bool[] _open = new bool[WindowCount]; private long _revision; public ChatWindowState() => ResetToDefaults(); /// /// Monotonic counter bumped on every filter or open-state change. /// Lets presentation caches (per-window transcript layout) detect a /// filter/visibility change without re-deriving it from the raw arrays. /// public long Revision => Interlocked.Read(ref _revision); /// /// Reset every window to retail's PostInit defaults (color-table /// doc §4). The high dword is always 0 for every window — Society /// (0x20) and the reserved slot (0x21) are opt-in only, /// matching retail. Windows 1-4 start closed; window 0 (main) is always /// open. /// public void ResetToDefaults() { lock (_gate) { // "everything 0x00-0x1F except 0x1A" (m_oldState 1/8) — genuinely // consulted for broadcast lines now that BroadcastTargetWindow is // distinct from MainWindowId (see class doc). _filters[0] = MainWindowDefaultFilter; // Speech, Tell, Speech_Direct_Send, Emote (m_oldState 2). _filters[1] = Floaty1DefaultFilter; // Social, Social_Send, Allegiance (m_oldState 3). _filters[2] = Floaty2DefaultFilter; // Fellowship (m_oldState 4). _filters[3] = Floaty3DefaultFilter; // Turbine General/Trade/LFG/Roleplay (m_oldState 5). _filters[4] = Floaty4DefaultFilter; _open[0] = true; for (int i = MinFloatingWindowId; i <= MaxFloatingWindowId; i++) _open[i] = false; Interlocked.Increment(ref _revision); } } public ulong GetFilter(int windowId) { ValidateWindowId(windowId); lock (_gate) return _filters[windowId]; } /// /// Set window 's 64-bit type filter — including /// the main window (id 0): CH6a/b REJECT-review SHOULD-FIX 2 drops /// the old no-op (see the class doc's /// paragraph). Retail's main window IS user-settable through the options /// page; only the settings UI to drive this is future work. /// public void SetFilter(int windowId, ulong filter) { ValidateWindowId(windowId); lock (_gate) { if (_filters[windowId] == filter) return; _filters[windowId] = filter; Interlocked.Increment(ref _revision); } } /// Main window (id 0) is always open. public bool IsOpen(int windowId) { ValidateWindowId(windowId); if (windowId == MainWindowId) return true; lock (_gate) return _open[windowId]; } /// No-op for the main window — it cannot be closed. public void SetOpen(int windowId, bool open) { ValidateWindowId(windowId); if (windowId == MainWindowId) return; lock (_gate) { if (_open[windowId] == open) return; _open[windowId] = open; Interlocked.Increment(ref _revision); } } /// /// Flip window 's open state and return the /// new value. Always returns for the main window /// (it cannot be toggled closed). /// public bool Toggle(int windowId) { ValidateWindowId(windowId); if (windowId == MainWindowId) return true; lock (_gate) { bool next = !_open[windowId]; _open[windowId] = next; Interlocked.Increment(ref _revision); return next; } } /// /// ChatInterface::TypeIsActive @0x004F2F10: does window /// 's filter accept retail /// ? /// Types >= 64 are never active (there is no such bit). /// public bool TypeIsActive(int windowId, uint logTextType) { ValidateWindowId(windowId); if (logTextType >= 64u) return false; ulong filter; lock (_gate) filter = _filters[windowId]; return ((1UL << (int)logTextType) & filter) != 0UL; } /// /// ChatInterface::RecvNotice_DisplayFinalStringInfo @0x004F4640's /// exact display predicate — see the class doc for the two branches. /// is either a real window id /// (0-4, explicit addressing) or /// (broadcast, filtered per window). /// public bool ShouldDisplay(int windowId, uint targetWindowId, uint logTextType) { ValidateWindowId(windowId); if (targetWindowId == (uint)windowId) return true; return targetWindowId == BroadcastTargetWindow && TypeIsActive(windowId, logTextType); } private static void ValidateWindowId(int windowId) { if (windowId < MainWindowId || windowId > MaxFloatingWindowId) throw new ArgumentOutOfRangeException( nameof(windowId), windowId, "chat window id must be 0 (main) through 4 (floating)."); } }