using System; using System.Numerics; using AcDream.Core.Social; namespace AcDream.App.UI.Layout; /// /// Campaign FA slice FA3, D1: the Squelch page's read-only roster — names /// only, bound directly to the J4.1 owner /// (RuntimeCommunicationState.Squelch). Lists both squelched /// characters (, keyed by the /// character's own display name) and squelched accounts /// (, whose dictionary KEY is the /// account name itself). Rebuilds on /// change; polled every frame from /// WHILE THE PANEL IS VISIBLE (see the rebuild-discipline paragraph below). /// /// /// D1 — inert actions. Same disposition as /// : the three buttons /// (0x10000547/0x1000054B/0x1000054C) are left /// AUTHORED and CLICKABLE with no handler. See register row AD-79 /// (docs/architecture/retail-divergence-register.md), which covers /// the whole D1 Friends/Squelch inert-actions scope. /// /// /// /// Row template 0x21000060/0x10000541 (FA3 live-mount probe); /// see for the name-text resolution note /// (gmSquelchUI is outside this campaign's decompiled scope). /// /// /// /// Scrollbar (fix-round blast MF-1; FA4 carry-forward 3). The /// ListBox 0x1000053E authors a direct sibling scrollbar, /// 0x10000543 — resolved via the built /// and wired to /// the same way /// wires its own (see that /// class's doc for the full rationale, including why there is no wheel /// fallback and why the authored field replaces a hardcoded literal). /// /// /// /// Rebuild discipline (fix-round blast SF-2/SF-3). Same discipline as /// : only /// advances after every row resolves, and /// gates on the /// panel's own visibility so no DAT-locked rebuild work runs while the /// panel is closed. /// /// public sealed class SocialSquelchPageController { private const uint ListBoxId = 0x1000053Eu; // 2026-08-13 gate (AD-79 retirement): probe-verified roles // (ProbeSocialClickRouting label dump) — the name box, the Remove button, // and the two add buttons ("Squelch Character" / "Squelch Account"). private const uint NameFieldId = 0x10000540u; private const uint RemoveButtonId = 0x10000547u; private const uint SquelchCharacterButtonId = 0x1000054Bu; private const uint SquelchAccountButtonId = 0x1000054Cu; /// 2026-08-13 (AD-79 retired for this page): add-by-name rides /// 0x0058 (character scope, guid 0, type AllChannels — ACE name- /// looks-up) / 0x0059 (account scope); remove is 0x0058/0x0059 /// with add=0 for the selected row. Wire details: /// docs/research/2026-08-13-social-wire-completion.md §1.5. public sealed record Actions( Action SquelchCharacter, Action SquelchAccount, Action RemoveCharacterSquelch, Action RemoveAccountSquelch); private readonly UiTemplateListBox _listBox; private readonly SquelchState _squelch; private readonly Actions? _actions; private readonly UiField? _nameField; private long _lastRevision = long.MinValue; private (uint Guid, string Name, bool IsAccount)? _selected; private SocialSquelchPageController( UiTemplateListBox listBox, SquelchState squelch, Actions? actions, UiField? nameField) { _listBox = listBox; _squelch = squelch; _actions = actions; _nameField = nameField; } public static SocialSquelchPageController? Bind( UiElement pageRoot, SquelchState squelch, Func templateResolver, Actions? actions = null) { ArgumentNullException.ThrowIfNull(pageRoot); ArgumentNullException.ThrowIfNull(squelch); ArgumentNullException.ThrowIfNull(templateResolver); if (UiElement.FindDescendant(pageRoot, ListBoxId) is not UiTemplateListBox listBox) { Console.WriteLine( $"[D.2b] SocialSquelchPageController: ListBox 0x{ListBoxId:X8} not " + "found — Squelch page will not populate."); return null; } listBox.TemplateResolver = templateResolver; uint scrollbarElementId = listBox.ScrollbarElementId; UiElement? scrollbarElement = scrollbarElementId == 0 ? null : UiElement.FindDescendant(pageRoot, scrollbarElementId); if (scrollbarElement is UiScrollbar scrollbar) scrollbar.Model = listBox.Scroll; else Console.WriteLine( $"[D.2b] SocialSquelchPageController: scrollbar 0x{scrollbarElementId:X8} " + "not found — the Squelch list will not scroll."); var nameField = UiElement.FindDescendant(pageRoot, NameFieldId) as UiField; var controller = new SocialSquelchPageController(listBox, squelch, actions, nameField); controller.WireActions(pageRoot); controller.Refresh(); return controller; } private void WireActions(UiElement pageRoot) { if (_actions is not { } actions) return; // fixture callers stay inert if (UiElement.FindDescendant(pageRoot, SquelchCharacterButtonId) is UiButton addCharacter) addCharacter.OnClick = () => { string name = _nameField?.Text?.Trim() ?? string.Empty; if (string.IsNullOrWhiteSpace(name)) return; actions.SquelchCharacter(name); _nameField?.SetText(string.Empty); }; if (UiElement.FindDescendant(pageRoot, SquelchAccountButtonId) is UiButton addAccount) addAccount.OnClick = () => { string name = _nameField?.Text?.Trim() ?? string.Empty; if (string.IsNullOrWhiteSpace(name)) return; actions.SquelchAccount(name); _nameField?.SetText(string.Empty); }; if (UiElement.FindDescendant(pageRoot, RemoveButtonId) is UiButton remove) remove.OnClick = () => { if (_selected is not { } selected) return; if (selected.IsAccount) actions.RemoveAccountSquelch(selected.Name); else actions.RemoveCharacterSquelch(selected.Guid, selected.Name); }; } public void Tick() { long revision = _squelch.Revision; if (revision == _lastRevision) return; Refresh(); } private void Refresh() { long revision = _squelch.Revision; _listBox.Flush(); SquelchDatabase database = _squelch.Snapshot(); bool allRowsResolved = true; bool selectedStillPresent = false; foreach ((uint guid, SquelchInfo character) in database.Characters) { allRowsResolved &= AddRow(character.Name, guid, isAccount: false); if (_selected is { IsAccount: false } s && s.Guid == guid) selectedStillPresent = true; } foreach (string accountName in database.Accounts.Keys) { allRowsResolved &= AddRow(accountName, 0u, isAccount: true); if (_selected is { IsAccount: true } s && s.Name == accountName) selectedStillPresent = true; } if (!selectedStillPresent) _selected = null; // SF-3: only latch the revision once the rebuild actually reflects it — // a resolver miss must not silently swallow a future revision bump. if (allRowsResolved) _lastRevision = revision; } private bool AddRow(string name, uint guid, bool isAccount) { UiElement? row = _listBox.AddItemFromTemplateList(0); if (row is null) return false; if (SocialPanelRowText.FindDeepest(row) is { } text) { text.LinesProvider = () => [new UiText.Line(name, Vector4.One)]; // 2026-08-13 gate: row click selects the entry the Remove button // acts on (UiText.OnClick clears the display-text ClickThrough // default). text.OnClick = () => _selected = (guid, name, isAccount); } return true; } }