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
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue