fix(ui): OP3 review fixes — byte-verified Magic chat lines, Gameplay/OptionPage shape, mid-air tri-state, shared geometry

Consolidated fix round for the two OP3 dual-lens reviews
(docs/research/2026-08-11-op3-review-{mechanism,blast}.md), both
APPROVE-WITH-FIXES.

MUST-FIX:
- The six "Use Mouse Turning Settings" chat lines were typed
  RetailLogTextType.ClientLocal (0x1A); retail types them 0x07 (Magic).
  BYTE-VERIFIED against the PDB-paired binary at all six
  gmConfigUI::SetMouseTurningDefaults call sites (0x0049E972/E9E2/EA52/
  EAA4/EAF6/EB48): every site pushes `6a 07` (type=7) immediately before
  the text-pointer push and the AddTextToScroll call. Added a dedicated
  OptionsRuntimeBindings.DisplayMouseTurningMacroLine seam routed at
  Magic (scrolling chat transcript, light blue, timestamped) instead of
  the 4-slot SpewBox ClientLocal uses; the mid-air refusal and UA/RA
  keep ClientLocal (both independently confirmed correct).
- Filed AD-77: the client-wide floating-only gmPanelUI host divergence
  (retail also exposes a docked 0x21000017 host) the plan §5 delegated
  to this review, scoped to every main panel, not just Options.

SHOULD-FIX:
- gmGameplayOptionsUI is not an OptionPage in retail (acclient.h:55857,
  UIElement_Field). OptionsPanelController now constructs the Gameplay
  slot's OptionPage with AfterApply deliberately null, so entering/
  leaving that tab never publishes SaveCharacterOptionsRuntimeCmd.
  Corrected OptionPageModel's doc comment and rewrote the two tests
  that pinned the wrong (Gameplay-flushes) shape.
- Added the OptionPage.OnOptionChanged seam (PlayerOptionPage::
  OnOptionChanged @0x004F27D0) — fires as the last step of Apply/
  Reset/Defaults, plus once per live LED edit via a new
  IOptionRow.AttachPageNotify hook (BoolOptionRow wires it into
  SetCurrentValue only, matching retail's Apply(1)-only
  HandleDialogAndNotices path). OP4-6 will bind Apply/Reset enable
  state to this.
- Exit to Character Selection's mid-air refusal is now tri-state
  (Func<bool?> IsGrounded): retail's UseTime only reaches the airborne
  test inside `else if (smartbox->player)`, so outside player mode (or
  with no live controller) the button is a SILENT no-op, not a
  refusal. Fixed the inverted comment at both call sites.
- Options panel geometry now matches its nine gmPanelUI siblings
  sharing RetailPanelUiController's one main-panel rectangle
  (ResizeX=false, bottom-edge-only resize, no invented Min/MaxWidth/
  Height) instead of being the only all-four-edge/horizontal-resize
  outlier whose width silently reverted whenever a sibling was shown.
- Added the three missing test pins: Options/Character mutual
  exclusion through a REAL RetailPanelUiController registration,
  RetailDialogFactory.MakeConfirmation's omitted-queueKey overload
  sharing DefaultQueueKey, and UiTabPanel.ActivePageChanged never
  firing on a dormant (non-activated) host.
- TS-74's What/Where now names the five store-only CameraTurning
  preferences explicitly instead of only mentioning them in Risk.
- Test script gains the toolbar-button ghosted->enabled+highlight
  check, UseMouseTurning-survives-relogin and the five prefs-survive-
  relaunch steps, a UA/RA legibility eye-item, and the corrected
  bottom-edge-only geometry description for step 5.

One-liners fixed in files already touched: symmetric close-button
resolve-failure logging in OptionsPanelController.Bind (blast NOTE 8).

Full Release suite: 12,947 passed / 4 skipped / 0 failed (baseline
12,935/4/0 post-OP7 — 12 net new tests; the two OptionPageModelTests
"wrong-shape" tests were renamed/rewritten in place, not removed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-11 03:05:27 +02:00
parent 64898f1301
commit 386076af0f
11 changed files with 505 additions and 72 deletions

View file

@ -122,7 +122,21 @@ public sealed class OptionsPanelController : IRetainedPanelController
private OptionsPanelController(UiTabPanel tabPanel, Action? afterApply)
{
_tabPanel = tabPanel;
foreach (uint pageId in new[] { GameplayPageId, CharacterPageId, ChatPageId, ConfigPageId })
// Mechanism review S1 (2026-08-11 fix round): gmGameplayOptionsUI
// (acclient.h:55857) derives from UIElement_Field, NOT
// OptionPage/PlayerOptionPage at all — unlike the other three tabs
// (gmCharacterSettingsUI/gmChatOptionsUI/gmConfigUI, all
// : PlayerOptionPage). Retail never calls SaveCurrentValues for the
// Gameplay page, so it never flushes the batched blob on show/hide.
// The model still needs an (empty) page instance so
// OnActivePageChanged's TryGetValue lookup and OnHidden/OnShown
// (Reset/Apply over zero rows, both harmless no-ops) keep working
// uniformly across all four tabs — only AfterApply is deliberately
// left null here, so entering/leaving Gameplay never publishes
// SaveCharacterOptionsRuntimeCmd.
_pages.Add(GameplayPageId, new OptionPage { AfterApply = null });
foreach (uint pageId in new[] { CharacterPageId, ChatPageId, ConfigPageId })
{
var page = new OptionPage { AfterApply = afterApply };
_pages.Add(pageId, page);
@ -159,6 +173,10 @@ public sealed class OptionsPanelController : IRetainedPanelController
if (layout.FindElement(CloseButtonId) is UiButton close)
close.OnClick = callbacks.Toggle;
else
Console.WriteLine(
$"[D.2b] OptionsPanelController: close button 0x{CloseButtonId:X8} "
+ "not found in the built layout — its handler was not wired.");
BindButton(layout, ExitToCharacterSelectionId, callbacks.RequestExitToCharacterSelection);
// ConfigureKeyboardId: INERT this slice — authored, clickable, no