using System; using AcDream.App.UI; using AcDream.Core.Chat; namespace AcDream.App.UI.Layout; /// /// Campaign OP slice OP5 (2026-08-11): binds the Chat tab (LayoutDesc /// 0x2100005C, root 0x1000050A, ListBox 0x1000050D) — retail's /// gmChatOptionsUI::InitOptions @0x0049FC60 — through OP2's template-list /// mechanism and OP3's per-page model, exactly like /// . Six authored sections, byte-verified /// end-to-end against docs/research/2026-08-10-options-panel-structure.md §5 /// AND the raw gmChatOptionsUI::InitOptions/AddCheckboxBitfield64Option /// pseudo-C (research doc's own row table was cross-checked against BOTH the inline /// main/floaty4 call sites and the shared helper's switch — every mask, header /// key, separator, and the 12-vs-13-row Gameplay-row gap matched exactly): /// /// /// General Options — header, then TWO LINKED opacity sliders /// (Option_DefaultOpacity_Property 0x10000080 / Option_ActiveOpacity_Property /// 0x10000081), then a separator. /// Five per-window filter sections (main window id 8, floaties /// 2-5) — header, one block, separator. Main gets /// 12 rows (no "Gameplay" row); every floaty gets 13. /// /// /// /// The one 64-bit register, not 128 bits. Retail's own /// UIOption_CheckboxBitfield64::AddChild(this, uint64_t mask, uint32_t labelId, /// uint32_t tooltipId) signature (byte-verified from the pseudo-C function /// declaration, not just call-site operands) takes exactly ONE 64-bit mask — matching /// ChatWindowState's own single-ulong-per-window model (its committed /// defaults, e.g. main 0xFBFFFFFFu, are byte-identical to the research doc's /// "(high:low)" row/window tables once "high:low" is read as bits[63:32]:bits[31:0] of /// THAT one register — a BN push-immediate artifact for the 64-bit argument, not a /// genuine 128-bit two-register value). 's own /// CurrentLow/CurrentHigh pair is exactly that split, so every mask this /// controller feeds it is produced by from ONE canonical /// — never independently derived low/high literals. /// /// /// /// Retail window id vs acdream compact id. Retail's SetUserData argument /// (8 main, 2-5 floaties) is the id PlayerModule::SetChatWindowOption dispatches /// on; uses its OWN compact 0-4 range /// (Campaign CH slice CH6b). carries the /// mapping so nothing downstream needs to know retail's raw numbering. /// /// /// /// No store-only rows (AD-78 audit, user-directed, 2026-08-11, gate 2). /// Every row on this tab has a live acdream consumer — the two opacity /// sliders drive RetailWindowOpacityController (AP-190) through /// /, /// and all five filter blocks write directly, /// the SAME state CH6's floating chat windows read to decide what to show. /// Unlike the Character/Config tabs and Configure Keyboard, this controller /// has no caption-dimming to apply; verified by inspection rather than /// skipped. /// /// public static class ChatOptionsPageController { /// Chat page root — gmChatOptionsUI — the STANDALONE /// 0x2100005C layout's own root id. Only present as its own distinct node /// when 0x2100005C is imported directly (e.g. /// FixtureLoader.LoadOptionsChat()); when mounted through the tab host /// (0x2100006E/0x2100002B, what actually /// operates on), ElementReader.Merge's "derived id wins" rule means the /// PAGE SLOT keeps its own id () instead — use /// that one for any host-tree lookup. public const uint RootElementId = 0x1000050Au; /// The Chat page's SLOT element within the tab host /// (OptionsPanelController's own private ChatPageId) — the id that /// actually survives base-merge in the host-mounted tree /// operates on. Used to SCOPE the scrollbar lookup below. private const uint PageSlotElementId = 0x1000050Cu; /// The row ListBox (dat Type 5) — m_pOptionBox. public const uint ListBoxElementId = 0x1000050Du; /// The ListBox's linked scrollbar — shared with the Config tab (research /// doc §10.1: both tabs author the SAME scrollbar element id). public const uint ScrollbarElementId = 0x10000201u; private const int HeaderTemplateIndex = 0; private const int SeparatorTemplateIndex = 1; private const int UnlabelledSliderTemplateIndex = 3; private const int LabelledSliderTemplateIndex = 6; private const int BitfieldTemplateIndex = 8; private const uint StringTableId = 0x23000003u; /// #372 (2026-08-11): the ID_ChatOption_TextFilter_* /// label/tooltip family lives in its OWN string table — an exhaustive /// live-DAT sweep (OptionsPanelLiveMountProbeTests.ProbeFilterLabelHome) /// found the keys resolve ONLY in 0x2300000D, not the /// 0x23000003 options table the section headers and slider labels /// use (the research doc's §8 table-attribution was inferred, not /// dat-verified — the keys themselves were always right). All 13 filter /// rows rendered blank-captioned at the first connected gate because of /// this one wrong table id. private const uint FilterStringTableId = 0x2300000Du; /// The slider leaf inside either slider row template's subtree. private const uint SliderElementId = 0x1000021Cu; /// /// #380 (2026-08-11, gate 4): the slider row's own NAME-CAPTION text /// child ("Inactive Opacity" / "Active Opacity") — present on BOTH /// template idx3 and idx6 (research doc §1.5's template array: "label /// 0x1000021B + slider 0x1000021C", the SAME id /// already cites for its own /// slider rows sharing this template pool). Retail's /// PlayerOptionPage::AddSliderOption never sets this element's /// text itself (byte-verified — no StringInfo write in its /// pseudo-C body); the caption comes from /// UIOption_Slider::SetGameplayOptionProperty's own /// InqGameplayOptionNameAndTooltip catalog lookup instead — see /// 's own doc for the full byte /// trace. Nothing wired this element at all before #380 — the user's /// literal complaint ("I also miss the text next to the bars for /// Inactive and Active Opacity"). /// private const uint SliderLabelElementId = 0x1000021Bu; /// Labelled slider template's low/high range-caption children /// (research doc §1.5's template array; present only on template idx 6). private const uint SliderRangeMinElementId = 0x1000021Eu; private const uint SliderRangeMaxElementId = 0x1000021Fu; /// One authored filter row: its combined 64-bit mask (bits[63:32] = /// research doc's "high", bits[31:0] = "low") and retail's own /// ID_ChatOption_TextFilter_* symbol name (used verbatim as the hash key — /// "Allegience" is retail's OWN misspelling in the symbol; the STRING TABLE value /// it resolves to is properly spelled "Allegiance", per structure doc §10.4 item 8 /// and §5.2's own footnote). public readonly record struct FilterRowSpec(ulong Mask, string RetailLabelKey); /// /// The 13-row authored order, byte-verified against BOTH /// AddCheckboxBitfield64Option's shared tail (Combat..Error, always present) /// and the two inline blocks' own Gameplay-row prefix (main SKIPS it; window 5 /// INCLUDES it) — research doc §5.2. Row 0 (Gameplay) is the one /// gates per block. /// public static readonly FilterRowSpec[] FilterRows = { new(0x0000000083912021ul, "ID_ChatOption_TextFilter_Gameplay"), new(0x0000000000600040ul, "ID_ChatOption_TextFilter_Combat"), new(0x0000000000020080ul, "ID_ChatOption_TextFilter_Magic"), new(0x0000000000001004ul, "ID_ChatOption_TextFilter_AreaSpeech"), new(0x0000000000000018ul, "ID_ChatOption_TextFilter_Tells"), new(0x0000000000040C00ul, "ID_ChatOption_TextFilter_Allegience"), // retail's own misspelling new(0x0000000000080000ul, "ID_ChatOption_TextFilter_Fellowship"), new(0x0000000008000000ul, "ID_ChatOption_TextFilter_General"), new(0x0000000010000000ul, "ID_ChatOption_TextFilter_Trade"), new(0x0000000020000000ul, "ID_ChatOption_TextFilter_LFG"), new(0x0000000040000000ul, "ID_ChatOption_TextFilter_Roleplay"), new(0x0000000100000000ul, "ID_ChatOption_TextFilter_Society"), new(0x0000000004000000ul, "ID_ChatOption_TextFilter_Error"), }; /// One authored per-window filter block/section. public readonly record struct FilterBlockSpec( int RetailWindowId, int CompactWindowId, string HeaderKey, ulong DefaultFilter, bool IncludesGameplayRow); /// /// The five window sections in authored order — research doc §5/§5.2, byte- /// verified defaults matching 's own committed /// constants exactly (both sides derive from the SAME retail literals; this table /// reuses 's named constants rather than re-declaring /// them, so the two can never drift). /// public static readonly FilterBlockSpec[] FilterBlocks = { new(8, ChatWindowState.MainWindowId, "ID_ChatOption_MainChatWindow_Section", ChatWindowState.MainWindowDefaultFilter, IncludesGameplayRow: false), new(2, 1, "ID_ChatOption_FloatyChatWindow1_Section", ChatWindowState.Floaty1DefaultFilter, IncludesGameplayRow: true), new(3, 2, "ID_ChatOption_FloatyChatWindow2_Section", ChatWindowState.Floaty2DefaultFilter, IncludesGameplayRow: true), new(4, 3, "ID_ChatOption_FloatyChatWindow3_Section", ChatWindowState.Floaty3DefaultFilter, IncludesGameplayRow: true), new(5, 4, "ID_ChatOption_FloatyChatWindow4_Section", ChatWindowState.Floaty4DefaultFilter, IncludesGameplayRow: true), }; /// The live read/write seam every row on this page writes/reads /// through. Opacity routes to RetailWindowOpacityController (AP-190's /// live per-window fade); filters route to the shared /// CH6 already consumes. The two DAT-extracted defaults are resolved ONCE by the /// caller (RetailUiRuntime) — — since /// they never change within a session. /// /// OP5 review fix S1 (2026-08-11): / /// apply the value LIVE only — they must never /// perform the settings.json round trip themselves, or a slider drag (dozens of /// MouseMove ticks) becomes dozens of whole-file load+rewrite cycles on the /// UI thread. is the separate settle seam this /// controller calls exactly once per discrete edit (a Reset/Defaults click, or a /// drag's own MouseUp via ) — never /// once per tick — matching retail's own dirty-timer batching (structure doc /// §3.5) for this blob. /// public sealed record Bindings( Func CurrentDefaultOpacity, Func CurrentActiveOpacity, Action SetDefaultOpacity, Action SetActiveOpacity, Action FlushOpacity, float DefaultOpacityDatDefault, float ActiveOpacityDatDefault, Func CurrentFilter, Action SetFilter, // #380: the two sliders' row captions/tooltips, resolved ONCE by the // caller (RetailUiRuntime) via ChatOptionsDatCaptions — the SAME // "resolve once, thread the pre-resolved value through Bindings" // shape DefaultOpacityDatDefault/ActiveOpacityDatDefault already // established for the value half of this exact DAT lookup family. // Null when the DAT read failed or that entry's own StringInfo did // not resolve — the row then renders with NO caption rather than // invented English, matching this codebase's uniform degrade rule. ChatOptionsDatCaptions.Caption DefaultOpacityCaption, ChatOptionsDatCaptions.Caption ActiveOpacityCaption); /// /// Builds the General Options header + 2 sliders + 5 filter sections (header + /// block each) into 's Chat ListBox, links its /// scrollbar, seeds every row's current/default state, and registers each row /// into . /// /// The already-built Options panel tree — the Chat page's /// ListBox is a descendant of it via the tab host's page-slot base-merge (same /// import used). /// The Chat tab's model /// (). /// Builds one row template's subtree — wired onto /// the ListBox's AND called /// directly for the five bitfield blocks (bypassing /// 's own resolve step — /// see for why). /// Resolves ONE ID_ChatOption_* string by /// (table id, hash) — never hard-coded English. /// The live read/write seam for every row. /// if the Chat page's ListBox did not resolve as /// a . public static bool Bind( ImportedLayout layout, OptionPage page, Func templateResolver, Func resolveString, Bindings bindings) { ArgumentNullException.ThrowIfNull(layout); ArgumentNullException.ThrowIfNull(page); ArgumentNullException.ThrowIfNull(templateResolver); ArgumentNullException.ThrowIfNull(resolveString); ArgumentNullException.ThrowIfNull(bindings); if (layout.FindElement(ListBoxElementId) is not UiTemplateListBox listBox) { Console.WriteLine( $"[D.2b] ChatOptionsPageController: ListBox 0x{ListBoxElementId:X8} " + "not found (or not a UiTemplateListBox) in the built Options panel tree — " + "the Chat tab will have no rows."); return false; } listBox.TemplateResolver = templateResolver; // ScrollbarElementId (0x10000201) is SHARED with the Config tab (research // doc §10.1 — both tabs author the same scrollbar element id). ImportedLayout's // FindElement is a flat id->widget dictionary (last-build-wins on a // collision), so a plain layout.FindElement(ScrollbarElementId) here can // silently wire THIS scroll model onto Config's own scrollbar instance // instead of Chat's — scope the search to the Chat page's own subtree // instead (same "scoped search for a shared id" shape // OptionsPanelController uses for the Apply/Reset/Defaults buttons). Scoped // from PageSlotElementId, NOT RootElementId — see that field's own doc for // why the standalone layout's root id does not survive base-merge here. UiElement? chatPageSlot = layout.FindElement(PageSlotElementId); UiElement? scrollbarElement = chatPageSlot is null ? null : UiElement.FindDescendant(chatPageSlot, ScrollbarElementId); if (scrollbarElement is UiScrollbar scrollbar) scrollbar.Model = listBox.Scroll; else Console.WriteLine( $"[D.2b] ChatOptionsPageController: scrollbar 0x{ScrollbarElementId:X8} " + $"not found under Chat page slot 0x{PageSlotElementId:X8} — the Chat " + "tab's row list will not scroll."); BuildHeaderRow(listBox, "ID_ChatOption_GeneralOptions_Section", resolveString); BuildOpacitySliders(listBox, page, resolveString, bindings); BuildSeparatorRow(listBox); foreach (FilterBlockSpec spec in FilterBlocks) { BuildHeaderRow(listBox, spec.HeaderKey, resolveString); BuildFilterBlock(listBox, spec, page, templateResolver, resolveString, bindings); BuildSeparatorRow(listBox); } return true; } private static void BuildHeaderRow( UiTemplateListBox listBox, string headerKey, Func resolveString) { if (listBox.AddItemFromTemplateList(HeaderTemplateIndex) is not UiText header) { Console.WriteLine( $"[D.2b] ChatOptionsPageController: header template did not build as " + $"UiText for '{headerKey}'."); return; } string? label = resolveString(StringTableId, DatStringResolver.ComputeHash(headerKey)); if (label is null) { Console.WriteLine( $"[D.2b] ChatOptionsPageController: header string '{headerKey}' did not " + "resolve from the DAT string table — the row renders with no text rather " + "than an invented label."); return; } header.LinesProvider = () => new[] { new UiText.Line(label, header.DefaultColor) }; } private static void BuildSeparatorRow(UiTemplateListBox listBox) { if (listBox.AddItemFromTemplateList(SeparatorTemplateIndex) is null) Console.WriteLine("[D.2b] ChatOptionsPageController: separator template did not build."); } /// /// Slider 1 (template idx 3, unlabelled — Option_DefaultOpacity_Property) /// and slider 2 (template idx 6, labelled with the /// ID_UI_Value_Transparent/ID_UI_Value_Opaque range captions — /// Option_ActiveOpacity_Property). Retail's DualHash slider link /// (research doc §5.1) is ported through : /// dragging one applies through (which itself runs /// ChatOpacityLink and re-applies live), then pushes whatever the OTHER /// value became onto its own row/slider — raising one drags the other, never /// clamps. /// /// OP5 review fix M1 (2026-08-11): each row's own apply closure pushes its /// OWN slider's thumb from the post-link truth (bindings.Current*Opacity(), /// not the raw dragged value — the link may have clamped it), mirroring the OP4 /// binding pattern (a sibling widget's own edit pushes its own widget first). Without /// this, /RestoreDefaultValue /// (Reset/Defaults on ONLY this row) call apply but never /// refresh, leaving the thumb visually stuck at the pre-revert position. /// /// /// OP5 review fix S1 (2026-08-11): each row's own apply closure flushes the /// batched settings write () immediately UNLESS /// its own slider is mid-drag () — a Reset/ /// Defaults click is never mid-drag, so those keep writing immediately (single /// discrete edit, same as before); a drag tick IS mid-drag, so the write defers to /// the matching callback wired below, /// collapsing dozens of MouseMove-tick writes into exactly one per gesture. /// /// private static void BuildOpacitySliders( UiTemplateListBox listBox, OptionPage page, Func resolveString, Bindings bindings) { UiElement? row1 = listBox.AddItemFromTemplateList(UnlabelledSliderTemplateIndex); UiScrollbar? slider1 = row1 is null ? null : UiElement.FindDescendant(row1, SliderElementId) as UiScrollbar; if (slider1 is null) Console.WriteLine( "[D.2b] ChatOptionsPageController: default-opacity slider template did not " + "build a UiScrollbar leaf."); UiElement? row2 = listBox.AddItemFromTemplateList(LabelledSliderTemplateIndex); UiScrollbar? slider2 = row2 is null ? null : UiElement.FindDescendant(row2, SliderElementId) as UiScrollbar; if (slider2 is null) Console.WriteLine( "[D.2b] ChatOptionsPageController: active-opacity slider template did not " + "build a UiScrollbar leaf."); if (row2 is not null) { SetRangeLabel(row2, SliderRangeMinElementId, "ID_UI_Value_Transparent", resolveString); SetRangeLabel(row2, SliderRangeMaxElementId, "ID_UI_Value_Opaque", resolveString); } // #380: the row-name captions ("Inactive Opacity" / "Active // Opacity") + tooltips — resolved once by the caller via // ChatOptionsDatCaptions, threaded through Bindings exactly like the // sibling DefaultOpacityDatDefault/ActiveOpacityDatDefault values. // Element 0x1000021B is shared by BOTH templates (see its own doc). if (row1 is not null) SetOpacityCaption(row1, bindings.DefaultOpacityCaption, slider1); if (row2 is not null) SetOpacityCaption(row2, bindings.ActiveOpacityCaption, slider2); if (slider1 is null || slider2 is null) return; float initialDefault = bindings.CurrentDefaultOpacity(); float initialActive = bindings.CurrentActiveOpacity(); slider1.SetScalarPosition(initialDefault); slider2.SetScalarPosition(initialActive); FloatOptionRow? defaultRow = null; FloatOptionRow? activeRow = null; defaultRow = new FloatOptionRow( initialDefault, bindings.DefaultOpacityDatDefault, apply: value => { bindings.SetDefaultOpacity(value); slider1.SetScalarPosition(bindings.CurrentDefaultOpacity()); // M1: own slider first activeRow!.RefreshFromLink(bindings.CurrentActiveOpacity()); if (!slider1.IsDragging) bindings.FlushOpacity(); // S1: settle now unless mid-drag }, read: bindings.CurrentDefaultOpacity, refresh: value => slider1.SetScalarPosition(value)); activeRow = new FloatOptionRow( initialActive, bindings.ActiveOpacityDatDefault, apply: value => { bindings.SetActiveOpacity(value); slider2.SetScalarPosition(bindings.CurrentActiveOpacity()); // M1: own slider first defaultRow!.RefreshFromLink(bindings.CurrentDefaultOpacity()); if (!slider2.IsDragging) bindings.FlushOpacity(); // S1: settle now unless mid-drag }, read: bindings.CurrentActiveOpacity, refresh: value => slider2.SetScalarPosition(value)); slider1.ScalarChanged = value => defaultRow.SetCurrentValue(value); slider2.ScalarChanged = value => activeRow.SetCurrentValue(value); // S1: the drag-end seam — flushes whatever the tick loop above deferred. slider1.DragCompleted = bindings.FlushOpacity; slider2.DragCompleted = bindings.FlushOpacity; page.Register(defaultRow); page.Register(activeRow); } private static void SetRangeLabel( UiElement row, uint elementId, string labelKey, Func resolveString) { if (UiElement.FindDescendant(row, elementId) is not UiText text) return; string? label = resolveString(StringTableId, DatStringResolver.ComputeHash(labelKey)); if (label is null) { Console.WriteLine( $"[D.2b] ChatOptionsPageController: range label '{labelKey}' did not " + "resolve — rendered with no text rather than invented English."); return; } text.LinesProvider = () => new[] { new UiText.Line(label, text.DefaultColor) }; } /// /// #380: sets one opacity slider row's own name-caption (element /// ) + tooltip from a pre-resolved /// — the DAT-catalog /// counterpart of (which resolves a /// COMPILED symbol key; this resolves a RUNTIME DAT entry, so it takes /// the already-resolved string rather than a key to hash). A null /// leaves the row with /// no caption rather than inventing English, matching every other /// resolve-miss path in this controller. The tooltip attaches to the /// SLIDER itself (the interactive/hoverable widget for this row — same /// convention 's own slider /// rows use), not the caption text. /// private static void SetOpacityCaption( UiElement row, ChatOptionsDatCaptions.Caption caption, UiScrollbar? slider) { if (UiElement.FindDescendant(row, SliderLabelElementId) is UiText text) { if (caption.Name is { Length: > 0 } name) text.LinesProvider = () => new[] { new UiText.Line(name, text.DefaultColor) }; else Console.WriteLine( "[D.2b] ChatOptionsPageController: opacity slider caption did not " + "resolve from the DAT name/tooltip catalog — the row renders with no " + "text rather than invented English."); } if (slider is not null && caption.Tooltip is { Length: > 0 } tooltip) slider.TooltipText = tooltip; } /// /// One per-window filter block: resolves the block template DIRECTLY through /// (NOT via /// — the block must be /// fully populated, growing to its final self-sized height (AP-195), BEFORE it is /// stacked into 's viewport), seeds it with retail's /// authored window default, adds every authored row (skipping "Gameplay" on the /// main window per ), THEN inserts /// it via . /// private static void BuildFilterBlock( UiTemplateListBox listBox, FilterBlockSpec spec, OptionPage page, Func templateResolver, Func resolveString, Bindings bindings) { if (BitfieldTemplateIndex >= listBox.Templates.Count) { Console.WriteLine( $"[D.2b] ChatOptionsPageController: bitfield template index " + $"{BitfieldTemplateIndex} missing from the Chat ListBox's authored " + $"template list — window {spec.RetailWindowId}'s filter block was not built."); return; } UiTemplateListEntry entry = listBox.Templates[BitfieldTemplateIndex]; UiElement? built = templateResolver(entry.TemplateLayoutId, entry.TemplateElementId); if (built is not UiCheckboxBitfield64 block) { Console.WriteLine( $"[D.2b] ChatOptionsPageController: filter block template did not build as " + $"UiCheckboxBitfield64 for window {spec.RetailWindowId}."); return; } // The block resolved fresh through templateResolver above — it needs its // OWN TemplateResolver wired (a SEPARATE property from the outer // listBox.TemplateResolver) before AddChild can resolve its per-row // template (0x10000521), exactly like the outer ListBox's own wiring a // few lines up in Bind(). block.TemplateResolver = templateResolver; (ulong defaultLow, ulong defaultHigh) = SplitMask(spec.DefaultFilter); block.SetDefaultValue(defaultLow, defaultHigh); for (int i = 0; i < FilterRows.Length; i++) { if (i == 0 && !spec.IncludesGameplayRow) continue; // research doc §5.2: the main window has no Gameplay row. FilterRowSpec rowSpec = FilterRows[i]; string? label = resolveString(FilterStringTableId, DatStringResolver.ComputeHash(rowSpec.RetailLabelKey)); string? tooltip = resolveString( FilterStringTableId, DatStringResolver.ComputeHash(rowSpec.RetailLabelKey + "_Desc")); if (label is null) Console.WriteLine( $"[D.2b] ChatOptionsPageController: filter row label " + $"'{rowSpec.RetailLabelKey}' did not resolve — row renders with no " + "caption rather than invented English."); (ulong low, ulong high) = SplitMask(rowSpec.Mask); if (block.AddChild(low, high, label ?? string.Empty, tooltip) is null) Console.WriteLine( $"[D.2b] ChatOptionsPageController: filter row '{rowSpec.RetailLabelKey}' " + $"did not build for window {spec.RetailWindowId}."); } listBox.AddPrebuiltRow(block); ulong initial = bindings.CurrentFilter(spec.CompactWindowId); (ulong initialLow, ulong initialHigh) = SplitMask(initial); block.SetCurrentValue(initialLow, initialHigh); // live seed, not the window default var row = new BitfieldOptionRow( initial, spec.DefaultFilter, apply: value => { (ulong low, ulong high) = SplitMask(value); block.SetCurrentValue(low, high); bindings.SetFilter(spec.CompactWindowId, value); }, read: () => bindings.CurrentFilter(spec.CompactWindowId), refresh: value => { (ulong low, ulong high) = SplitMask(value); block.SetCurrentValue(low, high); }); block.ValueChanged = (low, high) => row.SetCurrentValue(CombineMask(low, high)); page.Register(row); } /// Splits ONE 64-bit filter value into (bits[31:0], bits[63:32]) — /// the same split 's Low/High pair encodes (see /// this class's own doc for why that is not a genuine 128-bit value). private static (ulong Low, ulong High) SplitMask(ulong combined) => (combined & 0xFFFFFFFFul, (combined >> 32) & 0xFFFFFFFFul); /// Inverse of . private static ulong CombineMask(ulong low, ulong high) => ((high & 0xFFFFFFFFul) << 32) | (low & 0xFFFFFFFFul); }