feat(ui): Campaign OP slice OP5 — the Chat tab
Binds LayoutDesc 0x2100005C through OP2's template-list mechanism and OP3's per-page OptionPage model: the General Options header + two DualHash-linked opacity sliders (Option_DefaultOpacity_Property 0x10000080 / Option_ActiveOpacity_Property 0x10000081, live-apply on drag through RetailWindowOpacityController, defaults read from the installed DAT's DBProperties collection at DID 0x78000001 via ChatOptionsDatDefaults), and the five per-window text-filter blocks (main window 12 rows minus Gameplay, four floaties 13 rows each — the byte-verified authored order cross-checked against the raw gmChatOptionsUI::InitOptions/AddCheckboxBitfield64Option pseudo-C, not just the research doc's own table) writing AcDream.Core.Chat. ChatWindowState directly, the same state CH6's chat windows already read. AP-195 retired: ported both halves left open at the OP2 re-review — the ALL-set LED media swap (new UiButton.FaceFileOverride, driven by the block-level P0x10000082/P0x10000083 sprites now threaded through ElementInfo/DatWidgetFactory) and the CreateChildren self-sizing tail (UiCheckboxBitfield64.Height grows with its stacked row content; the enclosing ListBox reflows around the block's FINAL height via the new UiTemplateListBox.AddPrebuiltRow, reusing the ListBox's own stacking rather than a third stacking path). AP-187 broadened to cover the main window's own filter (previously only the four floaties) and the new live-editing write path. The main chat window's filter (retail window id 8, ChatWindowState id 0) gains its own settings.json persistence (ChatSettings. ChatWindowMainFilter) alongside the pre-existing floaty 1-4 fields; opacity persistence is now wired on every live slider change, not only through the old dev-scaffold Settings panel. Fixture regeneration (ACDREAM_REGENERATE_UI_FIXTURES=1) picked up the new ElementInfo.LedCheckedSprite/LedUncheckedSprite fields across all 19 committed layout fixtures — purely additive, confirmed against the live installed DAT (0x10000520's own 0x82/0x83 properties resolve to 0x06004D17/0x06004D19 exactly as AP-195 documented). Conformance: FilterRows/FilterBlocks pinned against the byte-verified authored order and ChatWindowState's own default constants; the AP-195 LED swap and self-sizing behavior; the DAT opacity-default extraction against the live installed DAT; live filter/opacity writes reaching ChatWindowState/RetailWindowOpacityController; OnShown re-seed and Reset/Defaults ghosting per the OP4 binding-pattern discipline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4798302226
commit
e71e5a9614
52 changed files with 4540 additions and 40 deletions
|
|
@ -70,6 +70,19 @@ public sealed class UiCheckboxBitfield64 : UiPanel
|
|||
/// </summary>
|
||||
public IReadOnlyList<UiTemplateListEntry> Templates { get; }
|
||||
|
||||
/// <summary>
|
||||
/// AP-195 (Campaign OP slice OP5): block-level "all mask bits set" LED media
|
||||
/// (dat property <c>0x10000082</c> on THIS element, <c>0x06004D17</c> 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).
|
||||
/// </summary>
|
||||
public uint CheckedLedSprite { get; }
|
||||
|
||||
/// <summary>AP-195 counterpart of <see cref="CheckedLedSprite"/>: block-level
|
||||
/// "checked but not every mask bit set" LED media (dat property
|
||||
/// <c>0x10000083</c>, <c>0x06004D19</c>).</summary>
|
||||
public uint UncheckedLedSprite { get; }
|
||||
|
||||
/// <summary>Same seam as <see cref="UiTemplateListBox.TemplateResolver"/> — a page
|
||||
/// controller wires this with real DAT access (Campaign OP slice OP5+). Left null
|
||||
/// by <c>DatWidgetFactory</c> itself.</summary>
|
||||
|
|
@ -84,12 +97,21 @@ public sealed class UiCheckboxBitfield64 : UiPanel
|
|||
// SpriteResolve (forwarded to each row's built subtree) is inherited from UiPanel —
|
||||
// same resolver shape, no need to redeclare it.
|
||||
|
||||
/// <summary>Fired with the new (low, high) value after any row toggles.</summary>
|
||||
/// <summary>Fired with the new (low, high) value after any row toggles — a LIVE
|
||||
/// user edit only (retail's <c>Apply(1)</c> path). NEVER fired by
|
||||
/// <see cref="SetCurrentValue"/> (the OnShown/Reset/Defaults re-seed path — pushing
|
||||
/// an externally-sourced value must not loop back into "the user changed this").
|
||||
/// </summary>
|
||||
public Action<ulong, ulong>? ValueChanged { get; set; }
|
||||
|
||||
public UiCheckboxBitfield64(IReadOnlyList<UiTemplateListEntry> templates)
|
||||
public UiCheckboxBitfield64(
|
||||
IReadOnlyList<UiTemplateListEntry> templates,
|
||||
uint checkedLedSprite = 0u,
|
||||
uint uncheckedLedSprite = 0u)
|
||||
{
|
||||
Templates = templates;
|
||||
CheckedLedSprite = checkedLedSprite;
|
||||
UncheckedLedSprite = uncheckedLedSprite;
|
||||
BackgroundColor = Vector4.Zero;
|
||||
BorderColor = Vector4.Zero;
|
||||
}
|
||||
|
|
@ -121,6 +143,24 @@ public sealed class UiCheckboxBitfield64 : UiPanel
|
|||
ValueChanged?.Invoke(CurrentLow, CurrentHigh);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes an externally-sourced (low, high) value onto the widget — the page-model
|
||||
/// re-seed path (retail's <c>Refresh</c> re-run after
|
||||
/// <c>OptionPage::RestoreSavedValues</c>/<c>RestoreDefaultValues</c>/the OnShown
|
||||
/// <c>SaveCurrentValue</c> re-read). Updates <see cref="CurrentLow"/>/
|
||||
/// <see cref="CurrentHigh"/> and every row's checked state + AP-195 LED media, but
|
||||
/// deliberately does NOT invoke <see cref="ValueChanged"/> — the caller (an
|
||||
/// <c>IOptionRow</c> 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 <c>BoolOptionRow</c>'s <c>refresh</c> delegate makes).
|
||||
/// </summary>
|
||||
public void SetCurrentValue(ulong low, ulong high)
|
||||
{
|
||||
CurrentLow = low;
|
||||
CurrentHigh = high;
|
||||
RefreshRowVisuals();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>UIOption_CheckboxBitfield64::AddChild(lowMask, highMask, labelId,
|
||||
/// tooltipId)</c> → <c>CreateChildren</c>'s per-row loop: instantiates the authored
|
||||
|
|
@ -174,7 +214,6 @@ public sealed class UiCheckboxBitfield64 : UiPanel
|
|||
// replaced by the typed mask closure below: equivalent routing,
|
||||
// without a stringly attribute round-trip.
|
||||
checkbox.TooltipText = tooltip;
|
||||
checkbox.Selected = IsSet(lowMask, highMask);
|
||||
checkbox.OnClick = () => ToggleRow(lowMask, highMask, checkbox);
|
||||
|
||||
row.Left = 0f;
|
||||
|
|
@ -182,7 +221,24 @@ public sealed class UiCheckboxBitfield64 : UiPanel
|
|||
_contentHeight += row.Height;
|
||||
base.AddChild(row);
|
||||
|
||||
_rows.Add(new Row(lowMask, highMask, label, tooltip, row, checkbox));
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
|
@ -199,18 +255,26 @@ public sealed class UiCheckboxBitfield64 : UiPanel
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>UIOption_CheckboxBitfield64::Refresh @0x004859C0</c>: a row is checked
|
||||
/// when ANY bit of its mask is set in the current value — NOT when every bit is
|
||||
/// set. <c>lowHit = current.low & mask.low; highHit = current.high &
|
||||
/// mask.high; checked = (lowHit | highHit) != 0</c>. This is HALF of Refresh:
|
||||
/// the same pass also computes the ALL-set predicate to swap each row's LED
|
||||
/// media between the two authored surfaces (<c>P0x10000082 = 0x06004D17</c> /
|
||||
/// <c>P0x10000083 = 0x06004D19</c>) — not ported yet, register row AP-195,
|
||||
/// due with the Chat tab (OP5) alongside the ListBox self-sizing tail.
|
||||
/// Retail <c>UIOption_CheckboxBitfield64::Refresh @0x004859C0</c>'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. <c>lowHit = current.low & mask.low; highHit =
|
||||
/// current.high & mask.high; checked = (lowHit | highHit) != 0</c>.
|
||||
/// </summary>
|
||||
private bool IsSet(ulong lowMask, ulong highMask)
|
||||
private bool IsAnySet(ulong lowMask, ulong highMask)
|
||||
=> (CurrentLow & lowMask) != 0 || (CurrentHigh & highMask) != 0;
|
||||
|
||||
/// <summary>
|
||||
/// AP-195: retail <c>Refresh</c>'s ALL-set half, computed by the SAME pass as
|
||||
/// <see cref="IsAnySet"/> — <c>(current.low & mask.low) == mask.low &&
|
||||
/// (current.high & mask.high) == mask.high</c>. Drives which of
|
||||
/// <see cref="CheckedLedSprite"/>/<see cref="UncheckedLedSprite"/> a checked row's
|
||||
/// LED shows: composite masks (e.g. the Gameplay row's <c>0x83912021</c>, 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.
|
||||
/// </summary>
|
||||
private bool IsAllSet(ulong lowMask, ulong highMask)
|
||||
=> (CurrentLow & lowMask) == lowMask && (CurrentHigh & highMask) == highMask;
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>UIOption_CheckboxBitfield64::ListenToElementMessage @0x00485AE0</c>:
|
||||
/// <c>BitUtils::SetBitsOnOrOff(&m_current, mask, onOff)</c> — decomp-confirmed
|
||||
|
|
@ -219,7 +283,7 @@ public sealed class UiCheckboxBitfield64 : UiPanel
|
|||
/// </summary>
|
||||
private void ToggleRow(ulong lowMask, ulong highMask, UiButton toggle)
|
||||
{
|
||||
bool turnOn = !IsSet(lowMask, highMask);
|
||||
bool turnOn = !IsAnySet(lowMask, highMask);
|
||||
if (turnOn)
|
||||
{
|
||||
CurrentLow |= lowMask;
|
||||
|
|
@ -230,13 +294,37 @@ public sealed class UiCheckboxBitfield64 : UiPanel
|
|||
CurrentLow &= ~lowMask;
|
||||
CurrentHigh &= ~highMask;
|
||||
}
|
||||
toggle.Selected = turnOn;
|
||||
ApplyRowVisualsForMask(lowMask, highMask, toggle);
|
||||
ValueChanged?.Invoke(CurrentLow, CurrentHigh);
|
||||
}
|
||||
|
||||
private void RefreshRowVisuals()
|
||||
{
|
||||
foreach (Row row in _rows)
|
||||
row.Toggle.Selected = IsSet(row.LowMask, row.HighMask);
|
||||
ApplyRowVisuals(row);
|
||||
}
|
||||
|
||||
private void ApplyRowVisuals(Row row) => ApplyRowVisualsForMask(row.LowMask, row.HighMask, row.Toggle);
|
||||
|
||||
/// <summary>
|
||||
/// One row's complete <c>Refresh</c> visual: the ANY-set checked bool
|
||||
/// (<see cref="UiButton.Selected"/>, always) plus AP-195's ALL-set-driven LED
|
||||
/// media swap (<see cref="UiButton.FaceFileOverride"/>) — retail's own gate: the
|
||||
/// swap ONLY applies while ANY-set is true; a fully-unchecked row keeps its
|
||||
/// authored per-row art untouched (<c>Refresh @0x004859C0</c>'s
|
||||
/// <c>if (ebx == 0) { ... media swap ... }</c> branch, where <c>ebx == 0</c> is
|
||||
/// the ANY-set case per the byte trace in this class's own research citations).
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue