fix: friends list live Online/Offline status - authored row state

machine + UiText per-state string swap (user retail gate)

The state side already worked (FriendsState replaces the full entry and
bumps Revision on 0x0021 OnlineStatus updates; the parser reads full
FriendData for every update type). The UI side had two gaps, exposed by
the installed-DAT row-template probe (template layout 0x2100005D root
0x10000519):

- The row authors TWO cells: the LEFT name text 0x1000051A whose
  Online (0x10000054) / Offline (0x10000055) PassToChildren states
  cascade into the RIGHT status grandchild 0x1000051F, which authors
  per-state 'Online'/'Offline' strings AND per-state colors (retail's
  green Online). The controller's FindDeepest binding wrote the NAME
  into the STATUS cell (the deepest text IS the status grandchild) and
  never flipped the state machine - so the status column never showed
  or updated anything.
- UiText had no per-state authored-string swap: ApplyDatState switched
  sprite + color per state but never the 0x17 string. Ported now
  (second consumer of the mechanism after the powerbar caption):
  DatWidgetFactory pre-resolves each state's authored string;
  TrySetRetailState swaps the line, colored by the SAME state's
  authored 0x1B.

SocialFriendsPageController now binds the name to its own cell and
flips the authored Online/Offline state per friend on every
Revision-driven rebuild - the cascade renders the status cell exactly
as retail's gmFriendsUI does, green Online included.

App suite 4,989/3 skips (new per-state swap conformance test).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-14 10:57:57 +02:00
parent 1390f9477d
commit e449c75e9f
5 changed files with 150 additions and 3 deletions

View file

@ -730,6 +730,47 @@ public class DatWidgetFactoryTests
Assert.Equal("six", lines[1].Text);
}
/// <summary>
/// 2026-08-14 friends gate: a Type-12 element whose OWN states author
/// 0x17 strings (the friends row's status cell: 'Online'/'Offline' with
/// per-state colors) swaps its displayed line on TrySetRetailState —
/// retail's state-cascade text swap.
/// </summary>
[Fact]
public void BuildText_PerStateAuthoredStrings_SwapWithRetailState()
{
var info = new ElementInfo { Type = 12, Width = 270, Height = 24 };
var online = new UiStateInfo { Id = 0x10000054u, Name = "Online" };
online.Properties.Values[0x17u] = new UiPropertyValue
{
Kind = UiPropertyKind.StringInfo,
// (Token, StringId, TableId, ...) — StringId 2 = 'Online'.
StringInfoValue = new UiStringInfoValue(0, 2, 1, 0, 1, 0),
};
online.Properties.Values[0x1Bu] = Color(0, 255, 0);
var offline = new UiStateInfo { Id = 0x10000055u, Name = "Offline" };
offline.Properties.Values[0x17u] = new UiPropertyValue
{
Kind = UiPropertyKind.StringInfo,
StringInfoValue = new UiStringInfoValue(0, 3, 1, 0, 1, 0),
};
info.States[0x10000054u] = online;
info.States[0x10000055u] = offline;
var text = Assert.IsType<UiText>(DatWidgetFactory.Create(
info, NoTex, null,
stringResolve: si => si.StringId == 2u ? "Online" : "Offline"));
Assert.True(text.TrySetRetailState(0x10000054u));
var line = Assert.Single(text.LinesProvider!());
Assert.Equal("Online", line.Text);
Assert.Equal(1f, line.Color.Y, 3); // authored green rides the state
Assert.True(text.TrySetRetailState(0x10000055u));
line = Assert.Single(text.LinesProvider!());
Assert.Equal("Offline", line.Text);
}
/// <summary>
/// 2026-08-14 gate: the stateful-fill meter (gmPowerbarUI's shape in
/// LayoutDesc 0x21000072) also absorbs its Type-12 caption child