fix: social panel completion batch (user gate 2026-08-13, "fix all")
One user-ordered batch across the FA social panel + world selection.
Every root cause was probe-proven before the fix (new
ProbeSocialClickRouting in SocialPanelLiveMountProbeTests - production
window mount + real UiRoot hit-tests + a synthetic click):
1. STUCK CHECKBOXES (fellowship x4, allegiance x1, "always checked /
can't change any options"): the authored checkboxes carry DAT
ToggleBehavior, so UiButton SELF-FLIPS Selected at MouseUp - the old
handlers read the flipped value and wrote the ORIGINAL back, snapping
every click to where it started (the probe recorded (id, oldValue)).
Fix: SuppressSelfToggle (the CH6a/b mirror discipline) + derive the
next value from the STORE; the per-tick seeding mirrors it back.
2. UNCLICKABLE ROSTER ROWS ("only get the move window cursor"): the row
name text is display-text ClickThrough=true, which the hit-test walk
skips regardless of HandlesClick - the wired OnClick was unreachable.
Fix: UiText.OnClick assignment now clears ClickThrough (central,
documented); the stats text gains the same select handler so most of
the row's width selects the fellow.
3. TRUNCATED EMPTY-STATE ("You do not belong... To create MISSING"):
the authored string resolves COMPLETE (three sentences) but embedded
'\n's rendered as one clipped line. DatWidgetFactory now splits
authored strings into one Line per newline, with the provider still
re-reading DefaultColor live (the state-color contract - caught by
BuildText_AuthoredLineTracksStateFontColor).
4. FELLOW NAMES WHITE (user-directed): the AD-82 invented leader-gold +
selection-blue tints are deleted; names always white (register row
narrowed).
5. ALLEGIANCE HEADER LABELS: bare "0"/"0" -> "Followers: N" / "Rank: [N]"
(user-specified format; the full retail StringInfo composition stays
AD-85's gap), monarch block matching.
6. FRIENDS/SQUELCH LIVE (AD-79 mostly retired): Add friend (name box ->
0x0018, retail clears the box - Request_AddFriend @0x0048D240),
Remove (row-click selection -> 0x0017), Appear Offline (CharacterOption
0x27 via the immediate 0x0005 auto-save, ACE pushes FriendStatusChanged
to your friend-of list), Squelch Character/Account add-by-name
(0x0058 guid0/type AllChannels + 0x0059) and Remove for the selected
row. The wire beneath (builders, WorldSession sends, Runtime commands,
parsers) existed end-to-end since J4.1/FA1 - this is panel wiring only
(docs/research/2026-08-13-social-wire-completion.md, committed here).
Send Tell stays inert (not in the order; AD-79's remainder).
7. WORLD SELF-SELECTION ("clicking my own char should select myself"):
retail has NO self-exclusion (CPhysicsPart::Draw @0x0050D823 arms
every physobj; RecvNotice_SmartBoxObjectFound @0x004E5BAE selects
unconditionally) - the includeSelf gate was an unregistered
divergence, now removed on both the left-click and right-click paths.
Element roles were probe-measured, never guessed (Add 0x10000514 /
Remove 0x10000515 / Send Tell 0x10000516 / Appear Offline 0x1000052C /
name field 0x1000051B; Squelch: field 0x10000540, Remove 0x10000547,
Squelch Character 0x1000054B, Squelch Account 0x1000054C).
Register: AD-79 mostly retired, AD-82 narrowed. Known remainder, filed
not hidden: the fellowship page's authored 600px content vs the 362px
viewport leaves Dismiss/Assign-Leader below the fold until the window is
resized taller (probe-measured; candidate follow-up).
Tests: Checkbox_Click fact rewritten to the mirror contract (both
directions), monarch-followers label updated, includeSelf expectation
updated, probe extended (click routing, synthetic click, action-widget
role dump). App suite 4,976/3 skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ec2a7b0cce
commit
72ceddce2e
15 changed files with 1018 additions and 65 deletions
|
|
@ -702,7 +702,19 @@ public static class DatWidgetFactory
|
|||
t.OutlineColor = info.OutlineColor.Value;
|
||||
|
||||
if (ResolveAuthoredString(info, stringResolve) is { Length: > 0 } authored)
|
||||
t.LinesProvider = () => [new UiText.Line(authored, t.DefaultColor)];
|
||||
{
|
||||
// 2026-08-13 social gate: authored strings can carry embedded
|
||||
// newlines (the fellowship empty-state is three sentences over
|
||||
// '\n's). A single Line renders them as one clipped run — split
|
||||
// into one Line per authored line, exactly as retail's multiline
|
||||
// UIElement_Text draws them. The provider re-reads DefaultColor
|
||||
// per call (NOT captured eagerly) so state-driven font-color
|
||||
// changes keep tracking, the same live-color contract the
|
||||
// single-line provider always had.
|
||||
string[] parts = [.. authored.Split('\n').Select(static p => p.TrimEnd('\r'))];
|
||||
t.LinesProvider = () =>
|
||||
[.. parts.Select(p => new UiText.Line(p, t.DefaultColor))];
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -421,10 +421,16 @@ public sealed class SocialAllegiancePageController
|
|||
if (tooltip is not null)
|
||||
_ignoreRequestsCheckbox.TooltipText = tooltip;
|
||||
|
||||
// 2026-08-13 gate fix ("can't press Ignore Allegiance requests —
|
||||
// always checked"): identical double-toggle to the fellowship page's
|
||||
// checkboxes — the authored ToggleBehavior self-flip made the old
|
||||
// `!Selected` read the flipped value and write the ORIGINAL back.
|
||||
// Same CH6a/b mirror discipline: suppress the self-flip, derive the
|
||||
// next value from the STORE, per-tick seeding mirrors the outcome.
|
||||
_ignoreRequestsCheckbox.SuppressSelfToggle = true;
|
||||
_ignoreRequestsCheckbox.OnClick = () =>
|
||||
{
|
||||
bool next = !_ignoreRequestsCheckbox.Selected;
|
||||
_ignoreRequestsCheckbox.Selected = next;
|
||||
bool next = !_bindings.CurrentCharacterOption(CharacterOptionId.IgnoreAllegianceRequests);
|
||||
_bindings.SetCharacterOption(CharacterOptionId.IgnoreAllegianceRequests, next);
|
||||
};
|
||||
}
|
||||
|
|
@ -518,8 +524,12 @@ public sealed class SocialAllegiancePageController
|
|||
private void RefreshSelfBlock(RuntimeAllegianceSnapshot snapshot)
|
||||
{
|
||||
SetLine(_selfName, ref _lastSelfName, snapshot.AllegianceName, TextColor);
|
||||
SetLine(_selfFollowers, ref _lastSelfFollowers, snapshot.TotalVassals.ToString(), TextColor);
|
||||
SetLine(_selfRank, ref _lastSelfRank, snapshot.Rank.ToString(), TextColor);
|
||||
// 2026-08-13 gate ("On top it just says 0 then 0"): the bare numbers
|
||||
// gain their labels in the user-specified format — the full retail
|
||||
// StringInfo composition for these fields remains AD-85's gap; this
|
||||
// is its user-directed partial fill.
|
||||
SetLine(_selfFollowers, ref _lastSelfFollowers, $"Followers: {snapshot.TotalVassals}", TextColor);
|
||||
SetLine(_selfRank, ref _lastSelfRank, $"Rank: [{snapshot.Rank}]", TextColor);
|
||||
}
|
||||
|
||||
// gmAllegianceUI::UpdateMonarchData @0x00491B40 — see class doc.
|
||||
|
|
@ -541,10 +551,11 @@ public sealed class SocialAllegiancePageController
|
|||
|
||||
RuntimeAllegianceMemberSnapshot monarchData = monarch!.Value;
|
||||
SetLine(_monarchName, ref _lastMonarchName, monarchData.Name, TextColor);
|
||||
// 2026-08-13 gate: same label treatment as the self block's followers.
|
||||
SetLine(
|
||||
_monarchFollowers,
|
||||
ref _lastMonarchFollowers,
|
||||
(snapshot.TotalMembers >= 1u ? snapshot.TotalMembers - 1u : 0u).ToString(),
|
||||
$"Followers: {(snapshot.TotalMembers >= 1u ? snapshot.TotalMembers - 1u : 0u)}",
|
||||
TextColor);
|
||||
_monarchField.Enabled = monarchData.IsLoggedIn;
|
||||
|
||||
|
|
|
|||
|
|
@ -58,11 +58,11 @@ namespace AcDream.App.UI.Layout;
|
|||
/// <b>Leader marker.</b> Lane A's row-template inventory names no dedicated
|
||||
/// "this fellow is the leader" element (the 8-child row is name / stats /
|
||||
/// three meter+text pairs, nothing else) — retail may simply not mark
|
||||
/// leadership in the row at all. Lacking a decompiled anchor either way,
|
||||
/// this controller tints the leader's name text a distinct gold
|
||||
/// (<see cref="LeaderNameColor"/>) as a minimal, clearly-adaptive visual
|
||||
/// cue rather than inventing a DAT mechanism that was not found — register
|
||||
/// row AD-82.
|
||||
/// leadership in the row at all. The earlier gold leader tint and blue
|
||||
/// selection tint this controller invented for that gap were RETIRED at the
|
||||
/// 2026-08-13 gate by user direction: names render white, always
|
||||
/// (<see cref="MemberNameColor"/>), and selection feedback is the in-game
|
||||
/// selection itself — register row AD-82 updated accordingly.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
|
|
@ -126,18 +126,12 @@ public sealed class SocialFellowshipPageController
|
|||
/// <c>_currNum >= 9</c> (lane B §7.1).</summary>
|
||||
private const int MaxFellowshipSize = 9;
|
||||
|
||||
private static readonly Vector4 LeaderNameColor = new(1f, 0.84f, 0f, 1f);
|
||||
/// <summary>2026-08-13 gate (user-directed): fellow names are WHITE,
|
||||
/// always — the earlier AD-82 leader-gold and selection-blue tints are
|
||||
/// retired (no decompiled anchor ever existed for either); the in-game
|
||||
/// selection ring is the selection feedback.</summary>
|
||||
private static readonly Vector4 MemberNameColor = Vector4.One;
|
||||
|
||||
/// <summary>Fix-round MUST-FIX 4 — the panel-local "this row is selected"
|
||||
/// visual cue (see the class doc's world→panel selection note).
|
||||
/// Same disposition as <see cref="LeaderNameColor"/>: no decompiled
|
||||
/// anchor for a per-row selection sprite/marker was found, so this is a
|
||||
/// minimal, clearly-adaptive tint rather than an invented DAT mechanism
|
||||
/// — register row AD-82. Takes priority over <see cref="LeaderNameColor"/>
|
||||
/// when a row is both the leader and the current selection.</summary>
|
||||
private static readonly Vector4 SelectedNameColor = new(0.45f, 0.85f, 1f, 1f);
|
||||
|
||||
/// <summary>Cached per <see cref="RefreshFellowshipName"/> — only
|
||||
/// reassigned when the fellowship name actually changes (fix-round SF-1,
|
||||
/// the same zero-allocation-while-idle discipline
|
||||
|
|
@ -455,10 +449,19 @@ public sealed class SocialFellowshipPageController
|
|||
if (tooltip is not null)
|
||||
checkbox.TooltipText = tooltip;
|
||||
|
||||
// 2026-08-13 gate fix ("I can't change any options"): these authored
|
||||
// checkboxes carry DAT ToggleBehavior, so the button SELF-FLIPS
|
||||
// Selected at MouseUp — the old `!checkbox.Selected` here then read
|
||||
// the ALREADY-FLIPPED value and wrote the ORIGINAL back, visually
|
||||
// snapping every click to where it started (probe-proven:
|
||||
// ProbeSocialClickRouting recorded (id, oldValue)). Same CH6a/b
|
||||
// mirror discipline as the chat indicators: suppress the blind
|
||||
// self-flip, derive the next value from the STORE, and let the
|
||||
// per-tick seeding mirror the outcome back onto Selected.
|
||||
checkbox.SuppressSelfToggle = true;
|
||||
checkbox.OnClick = () =>
|
||||
{
|
||||
bool next = !checkbox.Selected;
|
||||
checkbox.Selected = next;
|
||||
bool next = !_bindings.CurrentCharacterOption(id);
|
||||
_bindings.SetCharacterOption(id, next);
|
||||
};
|
||||
}
|
||||
|
|
@ -725,6 +728,15 @@ public sealed class SocialFellowshipPageController
|
|||
uint guid = member.Guid;
|
||||
nameText.OnClick = () => SelectFellow(guid);
|
||||
}
|
||||
// 2026-08-13 gate fix ("I only get the move window cursor"):
|
||||
// give the STATS text the same click target so most of the row's
|
||||
// width selects the fellow (UiText.OnClick now clears the
|
||||
// display-text ClickThrough default — see UiText.OnClick's doc).
|
||||
if (widgets.Stats is { } statsText)
|
||||
{
|
||||
uint guid = member.Guid;
|
||||
statsText.OnClick = () => SelectFellow(guid);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (RuntimeFellowMemberSnapshot member in members)
|
||||
|
|
@ -738,13 +750,11 @@ public sealed class SocialFellowshipPageController
|
|||
if (widgets.Name is { } nameText)
|
||||
{
|
||||
string name = member.Name;
|
||||
// MUST-FIX 4 (fix round): selection takes priority over the
|
||||
// leader tint when a row is both — it's the more immediate,
|
||||
// user-driven state (see SelectedNameColor's own doc).
|
||||
Vector4 color = _selectedFellowGuid == member.Guid
|
||||
? SelectedNameColor
|
||||
: snapshot.LeaderGuid == member.Guid ? LeaderNameColor : MemberNameColor;
|
||||
nameText.LinesProvider = () => [new UiText.Line(name, color)];
|
||||
// 2026-08-13 gate (user-directed): fellow names render WHITE,
|
||||
// always — the AD-82 invented leader/selection tints are retired;
|
||||
// selection feedback is the in-game selection itself
|
||||
// (SelectFellow drives the world selection ring).
|
||||
nameText.LinesProvider = () => [new UiText.Line(name, MemberNameColor)];
|
||||
}
|
||||
|
||||
if (widgets.Stats is { } statsText)
|
||||
|
|
|
|||
|
|
@ -15,17 +15,15 @@ namespace AcDream.App.UI.Layout;
|
|||
/// fix-round visibility gate).
|
||||
///
|
||||
/// <para>
|
||||
/// <b>D1 — inert actions.</b> This campaign implements no Friends
|
||||
/// add/remove/appear-offline wire (the trivially-pinnable bar D1 sets was
|
||||
/// not met for this family — see the plan's D1 decision). The three
|
||||
/// buttons (<c>0x10000514</c>/<c>0x10000515</c>/<c>0x10000516</c>) and the
|
||||
/// "Appear Offline" checkbox (<c>0x1000052C</c>) are left AUTHORED and
|
||||
/// CLICKABLE with no handler — <see cref="DatWidgetFactory"/> already
|
||||
/// builds every button/checkbox with a null <c>OnClick</c> by default, so
|
||||
/// no explicit "leave unbound" code is needed here. See register row
|
||||
/// AD-79 (<c>docs/architecture/retail-divergence-register.md</c>), which
|
||||
/// covers the whole D1 Friends/Squelch inert-actions scope (one row, not
|
||||
/// one per button).
|
||||
/// <b>Actions LIVE as of 2026-08-13 (AD-79 retired for this page).</b>
|
||||
/// Add (name box → <c>0x0018</c>), Remove (selected row → <c>0x0017</c>),
|
||||
/// and Appear Offline (CharacterOption <c>0x27</c> via the immediate
|
||||
/// <c>0x0005</c> auto-save) are wired through <see cref="Actions"/> — the
|
||||
/// wire beneath existed end-to-end all along
|
||||
/// (docs/research/2026-08-13-social-wire-completion.md §4). Send Tell
|
||||
/// (<c>0x10000516</c>) remains the one inert button — not in the
|
||||
/// 2026-08-13 order and needing the chat-tell seam; tracked in AD-79's
|
||||
/// remainder.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
|
|
@ -68,21 +66,56 @@ namespace AcDream.App.UI.Layout;
|
|||
public sealed class SocialFriendsPageController
|
||||
{
|
||||
private const uint ListBoxId = 0x10000517u;
|
||||
// 2026-08-13 gate (AD-79 retirement): the authored action widgets, roles
|
||||
// probe-verified against the live DAT (ProbeSocialClickRouting label
|
||||
// dump): Add / Remove / Send Tell buttons, the Appear Offline checkbox,
|
||||
// and the name edit box gmFriendsUI's own Add path reads
|
||||
// (Request_AddFriend @0x0048D240 reads the box, sends 0x0018, clears it).
|
||||
private const uint AddButtonId = 0x10000514u;
|
||||
private const uint RemoveButtonId = 0x10000515u;
|
||||
private const uint AppearOfflineCheckboxId = 0x1000052Cu;
|
||||
private const uint NameFieldId = 0x1000051Bu;
|
||||
|
||||
/// <summary>The live wire seams (2026-08-13, AD-79 retired): every
|
||||
/// command below already existed end-to-end (builders, WorldSession
|
||||
/// sends, Runtime commands, inbound parsers —
|
||||
/// docs/research/2026-08-13-social-wire-completion.md §4); this page was
|
||||
/// the only missing link. AppearOffline is CharacterOption 0x27 riding
|
||||
/// the immediate 0x0005 auto-save — ACE pushes FriendStatusChanged to
|
||||
/// everyone who friended you (its §1.4).</summary>
|
||||
public sealed record Actions(
|
||||
Action<string> AddFriend,
|
||||
Action<uint> RemoveFriend,
|
||||
Func<bool> CurrentAppearOffline,
|
||||
Action<bool> SetAppearOffline);
|
||||
|
||||
private readonly UiTemplateListBox _listBox;
|
||||
private readonly FriendsState _friends;
|
||||
private readonly Actions? _actions;
|
||||
private readonly UiField? _nameField;
|
||||
private readonly UiButton? _appearOfflineCheckbox;
|
||||
private long _lastRevision = long.MinValue;
|
||||
private uint _selectedFriendGuid;
|
||||
|
||||
private SocialFriendsPageController(UiTemplateListBox listBox, FriendsState friends)
|
||||
private SocialFriendsPageController(
|
||||
UiTemplateListBox listBox,
|
||||
FriendsState friends,
|
||||
Actions? actions,
|
||||
UiField? nameField,
|
||||
UiButton? appearOfflineCheckbox)
|
||||
{
|
||||
_listBox = listBox;
|
||||
_friends = friends;
|
||||
_actions = actions;
|
||||
_nameField = nameField;
|
||||
_appearOfflineCheckbox = appearOfflineCheckbox;
|
||||
}
|
||||
|
||||
public static SocialFriendsPageController? Bind(
|
||||
UiElement pageRoot,
|
||||
FriendsState friends,
|
||||
Func<uint, uint, UiElement?> templateResolver)
|
||||
Func<uint, uint, UiElement?> templateResolver,
|
||||
Actions? actions = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(pageRoot);
|
||||
ArgumentNullException.ThrowIfNull(friends);
|
||||
|
|
@ -108,13 +141,52 @@ public sealed class SocialFriendsPageController
|
|||
$"[D.2b] SocialFriendsPageController: scrollbar 0x{scrollbarElementId:X8} "
|
||||
+ "not found — the Friends list will not scroll.");
|
||||
|
||||
var controller = new SocialFriendsPageController(listBox, friends);
|
||||
var nameField = UiElement.FindDescendant(pageRoot, NameFieldId) as UiField;
|
||||
var appearOffline = UiElement.FindDescendant(pageRoot, AppearOfflineCheckboxId) as UiButton;
|
||||
var controller = new SocialFriendsPageController(
|
||||
listBox, friends, actions, nameField, appearOffline);
|
||||
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, AddButtonId) is UiButton add)
|
||||
add.OnClick = () =>
|
||||
{
|
||||
string name = _nameField?.Text?.Trim() ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(name)) return;
|
||||
actions.AddFriend(name);
|
||||
// Retail clears the edit box on send (Request_AddFriend
|
||||
// @0x0048D240's own post-send clear).
|
||||
_nameField?.SetText(string.Empty);
|
||||
};
|
||||
|
||||
if (UiElement.FindDescendant(pageRoot, RemoveButtonId) is UiButton remove)
|
||||
remove.OnClick = () =>
|
||||
{
|
||||
if (_selectedFriendGuid != 0u)
|
||||
actions.RemoveFriend(_selectedFriendGuid);
|
||||
};
|
||||
|
||||
if (_appearOfflineCheckbox is { } checkbox)
|
||||
{
|
||||
// The same ToggleBehavior mirror discipline as the fellowship/
|
||||
// allegiance checkboxes (the 2026-08-13 double-toggle fix).
|
||||
checkbox.SuppressSelfToggle = true;
|
||||
checkbox.OnClick = () =>
|
||||
actions.SetAppearOffline(!actions.CurrentAppearOffline());
|
||||
}
|
||||
}
|
||||
|
||||
public void Tick()
|
||||
{
|
||||
if (_actions is { } actions && _appearOfflineCheckbox is { } checkbox)
|
||||
checkbox.Selected = actions.CurrentAppearOffline();
|
||||
|
||||
long revision = _friends.Revision;
|
||||
if (revision == _lastRevision) return;
|
||||
Refresh();
|
||||
|
|
@ -125,16 +197,24 @@ public sealed class SocialFriendsPageController
|
|||
long revision = _friends.Revision;
|
||||
_listBox.Flush();
|
||||
bool allRowsResolved = true;
|
||||
bool selectedStillPresent = false;
|
||||
foreach (FriendEntry friend in _friends.Snapshot())
|
||||
{
|
||||
UiElement? row = _listBox.AddItemFromTemplateList(0);
|
||||
if (row is null) { allRowsResolved = false; continue; }
|
||||
if (friend.Id == _selectedFriendGuid) selectedStillPresent = true;
|
||||
if (SocialPanelRowText.FindDeepest(row) is { } text)
|
||||
{
|
||||
string name = friend.Name;
|
||||
uint guid = friend.Id;
|
||||
text.LinesProvider = () => [new UiText.Line(name, Vector4.One)];
|
||||
// 2026-08-13 gate: row click selects the friend the Remove
|
||||
// button acts on (UiText.OnClick clears the display-text
|
||||
// ClickThrough default — see its doc).
|
||||
text.OnClick = () => _selectedFriendGuid = guid;
|
||||
}
|
||||
}
|
||||
if (!selectedStillPresent) _selectedFriendGuid = 0u;
|
||||
// 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;
|
||||
|
|
|
|||
|
|
@ -102,7 +102,11 @@ public sealed class SocialPanelController : IRetainedPanelController
|
|||
SocialAllegiancePageController.Bindings Allegiance,
|
||||
FriendsState Friends,
|
||||
SquelchState Squelch,
|
||||
Func<uint, uint, UiElement?> TemplateResolver);
|
||||
Func<uint, uint, UiElement?> TemplateResolver,
|
||||
// 2026-08-13 (AD-79 retirement): the live Friends/Squelch action
|
||||
// seams — null (fixture callers) keeps those pages read-only.
|
||||
SocialFriendsPageController.Actions? FriendsActions = null,
|
||||
SocialSquelchPageController.Actions? SquelchActions = null);
|
||||
|
||||
private readonly UiTabPanel _tabPanel;
|
||||
private readonly SocialFellowshipPageController? _fellowship;
|
||||
|
|
@ -202,10 +206,12 @@ public sealed class SocialPanelController : IRetainedPanelController
|
|||
: SocialAllegiancePageController.Bind(allegiancePage, callbacks.Allegiance);
|
||||
SocialFriendsPageController? friends = friendsPage is null
|
||||
? null
|
||||
: SocialFriendsPageController.Bind(friendsPage, callbacks.Friends, callbacks.TemplateResolver);
|
||||
: SocialFriendsPageController.Bind(
|
||||
friendsPage, callbacks.Friends, callbacks.TemplateResolver, callbacks.FriendsActions);
|
||||
SocialSquelchPageController? squelch = squelchPage is null
|
||||
? null
|
||||
: SocialSquelchPageController.Bind(squelchPage, callbacks.Squelch, callbacks.TemplateResolver);
|
||||
: SocialSquelchPageController.Bind(
|
||||
squelchPage, callbacks.Squelch, callbacks.TemplateResolver, callbacks.SquelchActions);
|
||||
|
||||
if (fellowshipPage is null)
|
||||
Console.WriteLine($"[D.2b] SocialPanelController: Fellowship page 0x{FellowshipPageId:X8} not found.");
|
||||
|
|
|
|||
|
|
@ -53,21 +53,49 @@ namespace AcDream.App.UI.Layout;
|
|||
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;
|
||||
|
||||
/// <summary>2026-08-13 (AD-79 retired for this page): add-by-name rides
|
||||
/// <c>0x0058</c> (character scope, guid 0, type AllChannels — ACE name-
|
||||
/// looks-up) / <c>0x0059</c> (account scope); remove is <c>0x0058/0x0059</c>
|
||||
/// with add=0 for the selected row. Wire details:
|
||||
/// docs/research/2026-08-13-social-wire-completion.md §1.5.</summary>
|
||||
public sealed record Actions(
|
||||
Action<string> SquelchCharacter,
|
||||
Action<string> SquelchAccount,
|
||||
Action<uint, string> RemoveCharacterSquelch,
|
||||
Action<string> 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)
|
||||
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<uint, uint, UiElement?> templateResolver)
|
||||
Func<uint, uint, UiElement?> templateResolver,
|
||||
Actions? actions = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(pageRoot);
|
||||
ArgumentNullException.ThrowIfNull(squelch);
|
||||
|
|
@ -93,11 +121,46 @@ public sealed class SocialSquelchPageController
|
|||
$"[D.2b] SocialSquelchPageController: scrollbar 0x{scrollbarElementId:X8} "
|
||||
+ "not found — the Squelch list will not scroll.");
|
||||
|
||||
var controller = new SocialSquelchPageController(listBox, squelch);
|
||||
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;
|
||||
|
|
@ -112,22 +175,38 @@ public sealed class SocialSquelchPageController
|
|||
SquelchDatabase database = _squelch.Snapshot();
|
||||
|
||||
bool allRowsResolved = true;
|
||||
foreach (SquelchInfo character in database.Characters.Values)
|
||||
allRowsResolved &= AddRow(character.Name);
|
||||
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);
|
||||
{
|
||||
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)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2861,7 +2861,33 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
ShowConfirmation: (message, completed) => ShowConfirmation(message, completed)),
|
||||
Friends: _bindings.Social.Friends,
|
||||
Squelch: _bindings.Social.Squelch,
|
||||
TemplateResolver: TemplateResolver);
|
||||
TemplateResolver: TemplateResolver,
|
||||
// 2026-08-13 (AD-79 retirement): the wire beneath every one of
|
||||
// these commands shipped with J4.1/FA1 — builders, WorldSession
|
||||
// sends, router registrations, inbound parsers (see
|
||||
// docs/research/2026-08-13-social-wire-completion.md §4); the
|
||||
// panel publishes onto the SAME bus every other surface uses.
|
||||
FriendsActions: new Layout.SocialFriendsPageController.Actions(
|
||||
AddFriend: name => _bindings.Options.CommandBus().Publish(
|
||||
new AddFriendRuntimeCmd(name)),
|
||||
RemoveFriend: guid => _bindings.Options.CommandBus().Publish(
|
||||
new RemoveFriendRuntimeCmd(guid)),
|
||||
CurrentAppearOffline: () => _bindings.Options.CurrentCharacterOption(
|
||||
(uint)CharacterOptionId.AppearOffline),
|
||||
SetAppearOffline: value => _bindings.Options.CommandBus().Publish(
|
||||
new SetSingleCharacterOptionRuntimeCmd(
|
||||
(uint)CharacterOptionId.AppearOffline, value))),
|
||||
SquelchActions: new Layout.SocialSquelchPageController.Actions(
|
||||
// Character scope, guid 0 (ACE name-looks-up), AllChannels —
|
||||
// research §1.5's exact retail body.
|
||||
SquelchCharacter: name => _bindings.Options.CommandBus().Publish(
|
||||
new ModifyCharacterSquelchRuntimeCmd(true, 0u, name, 1u)),
|
||||
SquelchAccount: name => _bindings.Options.CommandBus().Publish(
|
||||
new ModifyAccountSquelchRuntimeCmd(true, name)),
|
||||
RemoveCharacterSquelch: (guid, name) => _bindings.Options.CommandBus().Publish(
|
||||
new ModifyCharacterSquelchRuntimeCmd(false, guid, name, 1u)),
|
||||
RemoveAccountSquelch: name => _bindings.Options.CommandBus().Publish(
|
||||
new ModifyAccountSquelchRuntimeCmd(false, name))));
|
||||
|
||||
// A second DatLock scope (MountOptionsPanel's own precedent, above):
|
||||
// SocialFellowshipPageController.Bind resolves the Open/Close
|
||||
|
|
|
|||
|
|
@ -22,8 +22,25 @@ namespace AcDream.App.UI;
|
|||
/// </summary>
|
||||
public sealed class UiText : UiElement, IUiDatStateful
|
||||
{
|
||||
/// <summary>Optional base-element click notice used by authored text tabs.</summary>
|
||||
public Action? OnClick { get; set; }
|
||||
/// <summary>Optional base-element click notice used by authored text tabs.
|
||||
/// Assigning a handler also clears <see cref="UiElement.ClickThrough"/> —
|
||||
/// display text is click-through by default (the class doc's contract),
|
||||
/// which otherwise makes the handler unreachable: the hit-test walk skips
|
||||
/// click-through elements no matter what <see cref="HandlesClick"/> says
|
||||
/// (the 2026-08-13 social gate's unclickable fellowship roster rows,
|
||||
/// probe-proven in <c>ProbeSocialClickRouting</c>).</summary>
|
||||
public Action? OnClick
|
||||
{
|
||||
get => _onClick;
|
||||
set
|
||||
{
|
||||
_onClick = value;
|
||||
if (value is not null)
|
||||
ClickThrough = false;
|
||||
}
|
||||
}
|
||||
|
||||
private Action? _onClick;
|
||||
public override bool HandlesClick
|
||||
=> OnClick is not null || WheelScrollEnabled || base.HandlesClick;
|
||||
/// <summary>Dat element id for imported UIElement_Text widgets. 0 for synthesized text.</summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue