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;
}
}