From 35c40a9b568d7e4e44ec9f15f1c2bd7762199a5f Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 12 Aug 2026 03:43:48 +0200 Subject: [PATCH] =?UTF-8?q?fix(ui):=20allegiance=20page=20=E2=80=94=20hois?= =?UTF-8?q?t=20per-frame=20LinesProvider=20allocation,=20document=20the=20?= =?UTF-8?q?coarser=20empty-state=20gate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mechanism SHOULD-FIX 2 / blast SHOULD-FIX 4 (same finding, both reviews): SocialAllegiancePageController.Tick() closed over a local `lines` on every call — `() => lines` allocated a display class plus a delegate on EVERY frame, unconditionally, at ~2,000 allocations/second at the profile's measured FPS, in a hot loop the Modern Runtime slices spent whole commits driving to 0 B/frame. Hoisted two static readonly Func> providers (BlankLineProvider/ NoLinesProvider); Tick() now assigns the cached delegate reference — zero allocation while idle or active. Mechanism SHOULD-FIX 7: FA3's empty-state gate is coarser than the retail mechanism it is contracted against — gmAllegianceUI::UpdateMonarchData @0x00491B40 hides the monarch/patron blocks per-relationship (monarch block also hides when the monarch IS the viewer; patron block on the analogous test), while this shell gates both blocks on the single HasProfile flag. Not a MUST-FIX for FA3 (MF-2 in the same review means HasProfile is effectively always false for the whole FA3 gate, so nothing wrong is visible during this slice's own gate) — recorded instead as an explicit FA5 acceptance line in the plan's FA5 row so the gap cannot be silently lost, plus a class-doc note that FA5's real monarch/patron population must also change Tick()'s unconditional LinesProvider reassignment in the same commit or its content will be overwritten the next frame. Co-Authored-By: Claude Fable 5 --- ...26-08-11-fellowship-allegiance-campaign.md | 2 +- .../Layout/SocialAllegiancePageController.cs | 31 +++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/docs/plans/2026-08-11-fellowship-allegiance-campaign.md b/docs/plans/2026-08-11-fellowship-allegiance-campaign.md index 902c9699..00af2b2d 100644 --- a/docs/plans/2026-08-11-fellowship-allegiance-campaign.md +++ b/docs/plans/2026-08-11-fellowship-allegiance-campaign.md @@ -180,7 +180,7 @@ FA2, FA3, FA4, FA5, FA6, FA7. | FA2 | `RuntimeFellowshipState` + `RuntimeAllegianceState` (lane D's 8-edit template each); single-site inbound wiring serving BOTH hosts; typed commands/views; snapshot revisions; reset/reconnect semantics (~~session-scoped vs seed-latched~~ **[FA2 fix-round: both session-scoped — see D2's addendum]**); bot event surface via polling | automated only | | FA3 | The social panel shell: mount slot `0x1000018F` (catalog id 12), F3/F4 handlers, fixture dump of the slot (closes U3/U4/U6/U7/U10), tab activation, all four pages' empty states, Friends/Squelch read-only binding to J4.1 state | user (connected) | | FA4 | Fellowship page live: roster rows (adds `UiTemplateListBox` Flush/selection/row-instance-id — lane A sized this), the `0x00A6` show/hide declaration + vitals stream, create dialog (inline name field, shareXP), recruit/dismiss/quit/disband/leader + confirmations, share display per D5, option-row un-dims per D7 | user (connected) + bot | -| FA5 | Allegiance page live: ~~profile parse on show (`0x027B`)~~ **[FA2 re-review CF-1 correction, 2026-08-12: the DATA subscription is `0x001F AllegianceUpdateRequest(on)` — retail arms it at `PostInit`, `RecvNotice_PlayerDescReceived`, and the panel's visible branch, and `0x0020 AllegianceUpdate` is the sole owner-seeder after the FA2 MF-2 fix; `0x027B`'s response is text-only chat. FA5 wires `0x001F(on)` at those lifecycle points (off on hide per retail's visible branch) or the panel opens with chat text and no data]**; monarch/patron/self blocks, flat vassal list with the reversal rule honored, swear/break/kick + confirmations, ACE zeroed-field presentation scoped honestly (register row) | user (connected) + bot | +| FA5 | Allegiance page live: ~~profile parse on show (`0x027B`)~~ **[FA2 re-review CF-1 correction, 2026-08-12: the DATA subscription is `0x001F AllegianceUpdateRequest(on)` — retail arms it at `PostInit`, `RecvNotice_PlayerDescReceived`, and the panel's visible branch, and `0x0020 AllegianceUpdate` is the sole owner-seeder after the FA2 MF-2 fix; `0x027B`'s response is text-only chat. FA5 wires `0x001F(on)` at those lifecycle points (off on hide per retail's visible branch) or the panel opens with chat text and no data]**; monarch/patron/self blocks, flat vassal list with the reversal rule honored, swear/break/kick + confirmations, ACE zeroed-field presentation scoped honestly (register row). **[FA3 fix-round addendum, 2026-08-12 (mechanism SF-7): FA3 shipped a COARSER empty-state gate than retail's own — both the monarch and patron blocks hide/show together on the single `RuntimeAllegianceSnapshot.HasProfile` flag. Retail's `gmAllegianceUI::UpdateMonarchData @0x00491B40` gates PER-RELATIONSHIP: the monarch block additionally hides when the monarch IS the viewer, and the patron block hides on the analogous test. FA5 MUST widen `SocialPanelController.Callbacks.AllegianceSnapshot` (today `Func`) to reach the per-relationship data `IRuntimeAllegianceView.TryGetMonarch`/`TryGetPatron` already expose (`GameRuntimeGameplayViews.cs:200-204`) and implement the real gate — a monarch character must NOT see an empty, visible monarch block, and a patron-of-the-monarch character must NOT see an empty, visible patron block. `SocialAllegiancePageController.Tick` also reassigns `LinesProvider` UNCONDITIONALLY every frame today — FA5's real name population must change this method in the same commit or its content is overwritten the next frame.]** | user (connected) + bot | | FA6 | Bot-vs-ACE automated gates: second-account config (USER PREREQUISITE), role-discriminated policy, the 7 Runtime commands + 7 named assertions (decisive: the recruited/sworn bot's own snapshot flips), reconnect-idempotence | automated + bot-vs-ACE run | | FA7 | Closeout: register reconciliation, ISSUES sweep, CLAUDE.md Current-state paragraph, memory digest, the connected-gate test script (the campaign's stop condition) | user's final connected matrix | diff --git a/src/AcDream.App/UI/Layout/SocialAllegiancePageController.cs b/src/AcDream.App/UI/Layout/SocialAllegiancePageController.cs index c784e32e..da360b14 100644 --- a/src/AcDream.App/UI/Layout/SocialAllegiancePageController.cs +++ b/src/AcDream.App/UI/Layout/SocialAllegiancePageController.cs @@ -34,6 +34,20 @@ namespace AcDream.App.UI.Layout; /// picking the wrong instance (§6 DISCIPLINE, the campaign-OP /// 0x10000211-in-two-layouts lesson). /// +/// +/// +/// FA5 scope note (fix-round mechanism SF-7). This shell's gate is +/// coarser than retail's: gmAllegianceUI::UpdateMonarchData +/// @0x00491B40 hides the monarch block when there is no monarch OR the +/// monarch IS the viewer, and hides the patron block on the analogous +/// per-relationship test — this controller instead gates BOTH blocks on +/// the single flag. The +/// plan's FA5 row records the two per-relationship acceptance lines FA5 +/// owes. Also note for whoever lands FA5: reassigns +/// UNCONDITIONALLY every frame — FA5's +/// real monarch/patron name population must change this method at the +/// same time, or its content will be overwritten on the very next frame. +/// /// public sealed class SocialAllegiancePageController { @@ -42,6 +56,17 @@ public sealed class SocialAllegiancePageController private static readonly IReadOnlyList NoLines = []; + /// + /// Fix-round mechanism SF-2 / blast SF-4: hoisted + /// delegates. runs every frame regardless of panel + /// visibility (see class doc); the original () => lines closure + /// captured a local and allocated a display class PLUS a delegate on + /// every call. These two cached delegates make the per-frame + /// reassignment a plain field write — zero allocation while idle. + /// + private static readonly Func> BlankLineProvider = () => BlankLine; + private static readonly Func> NoLinesProvider = () => NoLines; + private readonly UiElement _monarchField; private readonly UiElement _patronField; private readonly UiText? _monarchName; @@ -105,8 +130,8 @@ public sealed class SocialAllegiancePageController _monarchField.Visible = hasProfile; _patronField.Visible = hasProfile; - IReadOnlyList lines = hasProfile ? NoLines : BlankLine; - if (_monarchName is not null) _monarchName.LinesProvider = () => lines; - if (_patronName is not null) _patronName.LinesProvider = () => lines; + Func> provider = hasProfile ? NoLinesProvider : BlankLineProvider; + if (_monarchName is not null) _monarchName.LinesProvider = provider; + if (_patronName is not null) _patronName.LinesProvider = provider; } }