using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.UI.Layout; namespace AcDream.App.UI; /// /// Retail UIOption_CheckboxBitfield64 (Type 0x10000044) — a block of /// individually toggleable mask checkboxes (the Chat tab's per-window text-filter /// block). /// /// /// OP2 rework (2026-08-11 — `docs/research/2026-08-11-op2-review-mechanism.md` /// MUST-FIX 3/4): the OP2 slice's class doc claimed the authored template /// (0x10000520 in 0x2100002B) "carries NO children and NO media" and /// synthesized a fake per-row (Type=1, invented /// RowHeight=14) for every added row. Both claims were refuted by the committed /// fixture: 0x10000520 authors its OWN row-template list (dat property /// 0x64{0x2100002B, 0x10000521}), and retail /// UIOption_CheckboxBitfield64::CreateChildren @0x00485DF0 builds every row by /// calling UIElement_ListBox::AddItemFromTemplateList(this, 0, nullptr) in a /// loop — i.e. UIOption_CheckboxBitfield64 genuinely IS a /// UIElement_ListBox: it calls the ListBox mechanism on itself /// (AddItemFromTemplateList/GetItem/CalculatePaperSize/ /// ListenToElementMessage). The template 0x10000521 is a 272×20 container /// holding checkbox 0x10000219 (260×14, Type 1) which itself holds the 13×13 /// five-state LED child 0x10000328 — every row's art/font/insets now come from /// that authored subtree via , the SAME seam /// exposes (this class does not wrap a /// instance because retail's block does not itself /// scroll — the ENCLOSING Chat page ListBox scrolls the whole option list, and this /// block is one composite row within it — but it reuses the identical /// resolve-and-stack row-instantiation shape). /// /// public sealed class UiCheckboxBitfield64 : UiPanel { /// /// Retail element id of the checkbox inside the authored row template /// (0x10000521's sole child). Hardcoded in retail's own C++ /// (GetChildRecursive(row, 0x10000219) inside CreateChildren), not /// something generically derivable from the template — ports that literal constant. /// public const uint TemplateCheckboxElementId = 0x10000219u; /// One added checkbox row. public readonly record struct Row( ulong LowMask, ulong HighMask, string Label, string? Tooltip, UiElement RowRoot, UiButton Toggle); private readonly List _rows = new(); private float _contentHeight; /// The rows added so far, in call order. public IReadOnlyList Rows => _rows; /// Current low 64 bits of the option's bitfield value. public ulong CurrentLow { get; private set; } /// Current high 64 bits of the option's bitfield value. public ulong CurrentHigh { get; private set; } private ulong _defaultLow, _defaultHigh; /// /// The authored row template (dat property 0x64 on THIS element). Retail authors /// exactly one entry, {0x2100002B, 0x10000521}, and reuses it for every row /// (AddItemFromTemplateList(this, 0, nullptr) called once per row). /// public IReadOnlyList Templates { get; } /// /// AP-195 (Campaign OP slice OP5): block-level "all mask bits set" LED media /// (dat property 0x10000082 on THIS element, 0x06004D17 on the /// installed DAT). 0 when the authoring template carries none — the LED override /// is then simply never applied (rows keep their per-row authored art). /// public uint CheckedLedSprite { get; } /// AP-195 counterpart of : block-level /// "checked but not every mask bit set" LED media (dat property /// 0x10000083, 0x06004D19). public uint UncheckedLedSprite { get; } /// Same seam as — a page /// controller wires this with real DAT access (Campaign OP slice OP5+). Left null /// by DatWidgetFactory itself. public Func? TemplateResolver { get; set; } /// Dat font for row labels — kept for callers that built rows before this /// rework shipped; unused now that labels are set directly on the resolved /// template's own checkbox widget, whose font already comes from the authored /// template. public UiDatFont? LabelFont { get; set; } // SpriteResolve (forwarded to each row's built subtree) is inherited from UiPanel — // same resolver shape, no need to redeclare it. /// Fired with the new (low, high) value after any row toggles — a LIVE /// user edit only (retail's Apply(1) path). NEVER fired by /// (the OnShown/Reset/Defaults re-seed path — pushing /// an externally-sourced value must not loop back into "the user changed this"). /// public Action? ValueChanged { get; set; } public UiCheckboxBitfield64( IReadOnlyList templates, uint checkedLedSprite = 0u, uint uncheckedLedSprite = 0u) { Templates = templates; CheckedLedSprite = checkedLedSprite; UncheckedLedSprite = uncheckedLedSprite; BackgroundColor = Vector4.Zero; BorderColor = Vector4.Zero; } /// /// Retail UIOption::SetDefaultValue(low, high): sets the restore-to-defaults /// target. Also seeds the current value while no rows exist yet (construction /// order mirrors retail: SetDefaultValue then N AddChild calls, so /// each row's initial checked state reads the seeded default). /// public void SetDefaultValue(ulong low, ulong high) { _defaultLow = low; _defaultHigh = high; if (_rows.Count == 0) { CurrentLow = low; CurrentHigh = high; } } /// Reverts to the last target and refreshes /// every row's checked state — the Defaults-button verb (research doc §3.3). public void RestoreDefaultValue() { CurrentLow = _defaultLow; CurrentHigh = _defaultHigh; RefreshRowVisuals(); ValueChanged?.Invoke(CurrentLow, CurrentHigh); } /// /// Pushes an externally-sourced (low, high) value onto the widget — the page-model /// re-seed path (retail's Refresh re-run after /// OptionPage::RestoreSavedValues/RestoreDefaultValues/the OnShown /// SaveCurrentValue re-read). Updates / /// and every row's checked state + AP-195 LED media, but /// deliberately does NOT invoke — the caller (an /// IOptionRow wrapper) already owns whatever notification it needs, and /// firing here would loop a re-seed back into "the user changed this" (the same /// live/refresh distinction BoolOptionRow's refresh delegate makes). /// public void SetCurrentValue(ulong low, ulong high) { CurrentLow = low; CurrentHigh = high; RefreshRowVisuals(); } /// /// Retail UIOption_CheckboxBitfield64::AddChild(lowMask, highMask, labelId, /// tooltipId)CreateChildren's per-row loop: instantiates the authored /// row template ([0], the only entry) through /// , locates its embedded checkbox /// () via retail's own /// GetChildRecursive, and stamps this row's label/click/checked-state onto /// it. / are already-resolved /// strings — string-table lookup is the caller's job (matches /// 's own already-resolved-Label convention). /// /// The row's checkbox widget, or null (loud-logged) when no template is /// authored, no resolver is wired, the resolver produced nothing, or the resolved /// subtree does not contain — any of which /// means retail's authored shape is missing or the resolver seam is unwired, not /// something this widget should silently paper over. public UiButton? AddChild(ulong lowMask, ulong highMask, string label, string? tooltip = null) { if (Templates.Count == 0) { Console.WriteLine("[D.2b] UiCheckboxBitfield64.AddChild: no authored row template (property 0x64 empty) — cannot build a row."); return null; } Func? resolver = TemplateResolver; if (resolver is null) { Console.WriteLine("[D.2b] UiCheckboxBitfield64.AddChild: TemplateResolver not wired yet — cannot build a row."); return null; } UiTemplateListEntry entry = Templates[0]; // retail always reuses template index 0 UiElement? row = resolver(entry.TemplateLayoutId, entry.TemplateElementId); if (row is null) { Console.WriteLine($"[D.2b] UiCheckboxBitfield64.AddChild: resolver returned null for template 0x{entry.TemplateLayoutId:X8}/0x{entry.TemplateElementId:X8}."); return null; } UiButton? checkbox = FindCheckboxRecursive(row); if (checkbox is null) { Console.WriteLine($"[D.2b] UiCheckboxBitfield64.AddChild: resolved row template did not contain checkbox 0x{TemplateCheckboxElementId:X8} — row will not respond to clicks."); return null; } checkbox.Label = label; // Retail CreateChildren @0x00485DF0 stamps the row tooltip via // SetTooltip (OP2 re-review §2.2). Its @0x00485E3E companion — the // SetAttribute_Int(cb, 0x10000084, i) row-INDEX stamp retail later // reads back to find which row a click meant — is deliberately // replaced by the typed mask closure below: equivalent routing, // without a stringly attribute round-trip. checkbox.TooltipText = tooltip; checkbox.OnClick = () => ToggleRow(lowMask, highMask, checkbox); row.Left = 0f; row.Top = _contentHeight; _contentHeight += row.Height; base.AddChild(row); // AP-195 self-sizing tail: retail's own CreateChildren ends with // ResizeTo(GetWidth(), CalculatePaperSize(0, -1)) because // UIOption_CheckboxBitfield64 IS a UIElement_ListBox (its PostInit // tail-calls UIElement_Scrollable::PostInit) — the block grows to fit its // rows instead of keeping its authored 272x100 extent. Width is untouched // (GetWidth() — the ListBox's own authored column width); only Height // grows, exactly matching the stacked _contentHeight this class already // tracks per row. A dedicated ListBox-unification pass (reusing // UiTemplateListBox's own viewport) remains future work per AP-195's own // disposition menu; this direct resize is the recorded-rationale half of // that menu — see UiTemplateListBox.AddPrebuiltRow for how a page // controller reflows the ENCLOSING ListBox around this widget's now-final // height instead of the pre-build authored one. Height = _contentHeight; var newRow = new Row(lowMask, highMask, label, tooltip, row, checkbox); _rows.Add(newRow); ApplyRowVisuals(newRow); return checkbox; } private static UiButton? FindCheckboxRecursive(UiElement node) { if (node.DatElementId == TemplateCheckboxElementId && node is UiButton button) return button; foreach (UiElement child in node.Children) { UiButton? found = FindCheckboxRecursive(child); if (found is not null) return found; } return null; } /// /// Retail UIOption_CheckboxBitfield64::Refresh @0x004859C0's ANY-set half: /// a row is checked when ANY bit of its mask is set in the current value — NOT /// when every bit is set. lowHit = current.low & mask.low; highHit = /// current.high & mask.high; checked = (lowHit | highHit) != 0. /// private bool IsAnySet(ulong lowMask, ulong highMask) => (CurrentLow & lowMask) != 0 || (CurrentHigh & highMask) != 0; /// /// AP-195: retail Refresh's ALL-set half, computed by the SAME pass as /// (current.low & mask.low) == mask.low && /// (current.high & mask.high) == mask.high. Drives which of /// / a checked row's /// LED shows: composite masks (e.g. the Gameplay row's 0x83912021, five /// bits) can be PARTIALLY satisfied — checked (any-set) but not fully (all-set) — /// which single-bit rows (e.g. "General") can never observably distinguish. /// private bool IsAllSet(ulong lowMask, ulong highMask) => (CurrentLow & lowMask) == lowMask && (CurrentHigh & highMask) == highMask; /// /// Retail UIOption_CheckboxBitfield64::ListenToElementMessage @0x00485AE0: /// BitUtils::SetBitsOnOrOff(&m_current, mask, onOff) — decomp-confirmed /// OR-in-on / AND-NOT-off (TS-72's toggle-math half was always correct; only the /// checked-state predicate above needed fixing). /// private void ToggleRow(ulong lowMask, ulong highMask, UiButton toggle) { bool turnOn = !IsAnySet(lowMask, highMask); if (turnOn) { CurrentLow |= lowMask; CurrentHigh |= highMask; } else { CurrentLow &= ~lowMask; CurrentHigh &= ~highMask; } ApplyRowVisualsForMask(lowMask, highMask, toggle); ValueChanged?.Invoke(CurrentLow, CurrentHigh); } private void RefreshRowVisuals() { foreach (Row row in _rows) ApplyRowVisuals(row); } private void ApplyRowVisuals(Row row) => ApplyRowVisualsForMask(row.LowMask, row.HighMask, row.Toggle); /// /// One row's complete Refresh visual: the ANY-set checked bool /// (, always) plus AP-195's ALL-set-driven LED /// media swap () — retail's own gate: the /// swap ONLY applies while ANY-set is true; a fully-unchecked row keeps its /// authored per-row art untouched (Refresh @0x004859C0's /// if (ebx == 0) { ... media swap ... } branch, where ebx == 0 is /// the ANY-set case per the byte trace in this class's own research citations). /// private void ApplyRowVisualsForMask(ulong lowMask, ulong highMask, UiButton toggle) { bool anySet = IsAnySet(lowMask, highMask); toggle.Selected = anySet; if (!anySet) { toggle.FaceFileOverride = null; return; } uint overrideSprite = IsAllSet(lowMask, highMask) ? CheckedLedSprite : UncheckedLedSprite; toggle.FaceFileOverride = overrideSprite != 0u ? overrideSprite : null; } }