Converged MUST-FIX: the six mouse-turning macro lines are typed ClientLocal 0x1A where retail's AddTextToScroll sites pass type 7 (Magic, light blue — mechanism lens decoded the stray [4]=7 writes at all six BN call sites); 0x1A additionally routes to the SpewBox where the 4-slot cap discards two of the six lines before they draw (blast lens). Fix: a typed message seam, Magic for the macro, ClientLocal retained for the mid-air refusal and AD-75; byte-verify the type argument during the fix. Mechanism: the Gameplay page is NOT an OptionPage in retail (gmGameplayOptionsUI : UIElement_Field, acclient.h:55857) — the auto-flush-on-visibility acdream gave it flushes the blob at moments retail would not, and two green tests pin the wrong shape; the OnOptionChanged enable-gating seam (@0x004F27D0 — Apply/Reset gated, Defaults never) is missing from the model; the mid-air refusal fires where retail is silent (non-player-mode/null controller). Blast: the plan-assigned docked-host (0x21000017) register ruling was not filed — deemed a divergence, row owed client-wide; Options is the only shared-geometry panel with ResizeX/four-edge/min-size, which the shared _mainPanelGeometry silently reverts when siblings show; three targeted seams lack pins (real-sibling mutual exclusion, DefaultQueueKey, dormant ActivePageChanged silence); UA/RA legibility flagged for the gate. Clean: exactly one toolbar button changed (previously ghosted), F11 same-action since K.1c, no queue key invented, settings additive. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
27 KiB
OP3 review — regression / blast-radius lens
Commit under review: 9d26ecc6 — feat(ui): Campaign OP slice OP3 —
Options panel shell, open paths, Gameplay tab
Reviewer lens: regression + blast radius (what ALREADY-SHIPPED behavior
can this change move?). Pure read-only; no build, no test run, no launch.
Date: 2026-08-11
Verdict: APPROVE-WITH-FIXES — 2 MUST-FIX, 4 SHOULD-FIX, 8 NOTE.
The slice's core structural choice is sound and low-blast: it reuses the
generic catalog/RetailPanelUiController/toolbar mechanisms rather than
adding a parallel path, and every seam it touches was already generic. The
two MUST-FIX items are both evidence/bookkeeping gaps (an unverified chat
routing decision with a demonstrable line-loss consequence, and a register
row the plan explicitly assigned to this review), not architectural breaks.
Enumeration A — the gmPanelUI panel catalog, before and after
src/AcDream.App/UI/RetailPanelCatalog.cs. Mounted is what
TryGetWindowName/TryGetPanelId resolve; every entry that also calls
RetailPanelUiController.RegisterMainPanel shares ONE parent rectangle
(RetailPanelUiController.cs:41-65, :284-314) and retail's
"one active child" exclusion (:139-197).
| panelId | const | window | mount site | Left/Top | ResizeX / ResizeY | resizable edges | Min W×H | authored RestorePrevious (0x10000049) |
|---|---|---|---|---|---|---|---|---|
| 3 | CharacterInformation |
characterInformation |
RegisterIndicatorDetailPanel (RetailUiRuntime.cs:1713) |
18 / 18 | false / true | Bottom | — | yes (fixture character_info_2100006E_10000183.json) |
| 4 | PositiveEffects |
positiveEffects |
MountEffects (:1519) |
right-edge / 18 | false / true | Bottom | — | yes (effects_positive_2100001B.json) |
| 5 | NegativeEffects |
negativeEffects |
MountEffects (:1519) |
right-edge / 18 | false / true | Bottom | — | yes |
| 7 | Inventory |
inventory |
MountInventory (:2307) |
root.Left / root.Top | false / true | Bottom | — | not read (3-arg overload → false) |
| 8 | LinkStatus |
linkStatus |
RegisterIndicatorDetailPanel |
18 / 18 | false / true | Bottom | — | yes (link_status_2100001D.json) |
| 9 | MiniGame |
miniGame |
RegisterIndicatorDetailPanel |
18 / 18 | false / true | Bottom | — | yes (mini_game_2100001E.json) |
| 11 | Character |
character |
MountCharacter (:2226) |
540 / 18 | false / true | Bottom | — | not read (3-arg overload → false) |
| 13 | Magic |
spellbook |
MountSpellbook (:1326) |
18 / 18 | false / true | Bottom | — | not read (3-arg overload → false) |
| 15 | Vitae |
vitae |
RegisterIndicatorDetailPanel |
18 / 18 | false / true | Bottom | — | yes (vitae_21000020.json) |
| 10 | Options (NEW) |
options |
MountOptionsPanel (:1984) |
150 / 80 | TRUE / true | all four | 300 × 200 | absent from the fixture → false |
Authored extents (fixture roots): every one of these panels is 300 wide; heights are 600 (spellbook, character) or 362 (inventory, vitae, character info, effects, options). So the shared-rectangle height coupling between 600-tall and 362-tall panels already existed pre-OP3; Options joins the 362 cohort and introduces no new height class.
Does registering Options change any existing panel's open/close/stacking behavior? No, with one caveat and one new asymmetry:
- Exclusion: unchanged in kind.
SetPanelVisibilityhides the previous active panel regardless of which sibling is being shown; Options is just one more sibling. No existing pair's behavior changes. - Deferred-restore (
RestorePrevious): Options does not author0x10000049(verified:grep -c 268435529 tests/.../fixtures/options_panel_2100006E_1000018D.json→ 0). PerRetailPanelUiController.cs:164-167a non-RestorePreviouspanel clears_deferredPanel. SoCharacter sheet → Character Info (defers sheet) → F11drops the pending sheet restore. This is byte-identical to what opening Inventory/Character/Spellbook already did in that same position (all three areRestorePrevious: false), so it is not a regression — but it is a newly-reachable route to that drop. - Geometry asymmetry (new): Options is the only main panel with
ResizeX = true, all-four-edge resize, and explicitMinWidth/MinHeight(RetailUiRuntime.cs:1970-1980). See SHOULD-FIX 2.
Enumeration B — toolbar panel-button routing, before and after
ToolbarController.PanelButtonIds (ToolbarController.cs:54-55) has always
carried all seven ids; each binds by its OWN authored 0x10000029 value
(:170-175), and BindPanelButtons (:374-392) sets
button.Enabled = isAvailable(panelId) with OnClick = null when not
available. isAvailable is
RetailPanelCatalog.TryGetWindowName(panelId, …) && WindowManager.TryGet(name)
(RetailUiRuntime.cs:679-686).
| element | authored panelId | in catalog pre-OP3? | pre-OP3 state | post-OP3 state |
|---|---|---|---|---|
0x10000197 |
12 | no | ghosted, OnClick = null |
unchanged |
0x10000198 |
13 (Magic) | yes | enabled → spellbook | unchanged |
0x10000199 |
11 (Character) | yes | enabled → character | unchanged |
0x1000055A |
25 | no | ghosted | unchanged |
0x1000019A |
16 | no | ghosted | unchanged |
0x1000019B |
10 | no | ghosted, Enabled=false, OnClick=null — clicking did NOTHING |
enabled → ToggleWindow("options") |
0x100001B1 |
7 (Inventory) | yes | enabled → inventory | unchanged |
Answer to "what did 0x1000019B do pre-OP3": it was authored with panel
id 10, discovered by the generic binder, found panel 10 absent from the
catalog, and was therefore ghosted with a null click handler. Nothing
happened on click. No test pinned that old behavior (the fixture table at
ToolbarControllerTests.cs:26-35 pins the element→panelId mapping only,
and PanelButtons_bindByDatPanelId_andGhostUnavailablePanels at :221
drives its own isAvailable predicate, not the real catalog), so nothing
pinning old behavior had to change. Exactly one button's routing changed.
SyncToolbarWindowButtons (RetailUiRuntime.cs:670-675) now also syncs
panel 10's Highlight/Normal state; SetPanelOpen (ToolbarController.cs:394)
early-returns per-id, so no other button's state is touched.
Degradation path is clean: if MountOptionsPanel bails (:1954, :1962),
the catalog still maps options→10 but _panelUi has no id-10 entry, so
TogglePanel(10) returns false (RetailPanelUiController.cs:141) and the
toolbar button re-ghosts (WindowManager.TryGet fails). F11 silently no-ops
rather than falling through to Host.ToggleWindow.
Enumeration C — F11 history
| commit / event | KeyBindings.RetailDefaults() |
InputAction.ToggleOptionsPanel routed to |
net effect of pressing F11 |
|---|---|---|---|
84512d3c (Phase K.1a) |
file created; retail preset not yet built | — | — |
da189103 (Phase K.1c, retail keymap cutover) |
adds KeyChord(F11, None) → ToggleOptionsPanel (now KeyBindings.cs:216), sourced from retail-default.keymap.txt:148 ToggleOptionsPanel [ "" [ 0 DIK_F11 ] ] |
_devTools.ToggleSettingsPanel() |
opened the ImGui-era Settings panel |
| Campaign V slice V11 | unchanged | DevToolsGameplayCommands.ToggleSettingsPanel() becomes an empty method (GameplayInputCommandController.cs:70-72, class doc :53-59) |
nothing |
9d26ecc6 (OP3) |
unchanged — the row already existed | _retained.ToggleOptionsPanel() → RetailUiRuntime.ToggleWindow("options") → _panelUi.TogglePanel(10) |
opens/closes the retail Options panel |
Verification of the implementer's claim (git log -L 216,216:…/KeyBindings.cs):
the binding was introduced by da189103, not by OP3. OP3 touches
KeyBindings.cs not at all — only the new pin in
KeyBindingsRetailTests.cs:239. The claim is accurate.
What stopped working: nothing. Between V11 and OP3, F11 was a hard no-op.
Keybind conflicts: grep -n "F11" KeyBindings.cs returns exactly one row.
No chord conflict introduced. InputScope puts F11 at the bottom of the
stack (InputScope.cs:19) — unchanged.
Dead ToggleSettingsPanel wiring: untouched, as claimed — but now
orphaned. GameplayInputCommandController was its only production caller;
post-OP3 the only remaining references are the interface declaration
(:50), the empty implementation (:70), a comment (:242), and the test
fake (GameplayInputCommandControllerTests.cs:141). The commit comment says
"its no-op wiring elsewhere is untouched" — there is no elsewhere. Planned
for retirement in OP9; see NOTE 8.
Findings
MUST-FIX 1 — the six mouse-turning chat lines are routed to a 4-slot transient overlay on an unverified retail log type, and two of them can never be seen
MouseTurningSettingsMacro.Compute can emit six lines
(MouseTurningSettingsMacro.cs:58-104). RetailUiRuntime.ApplyMouseTurningSettingsMacro
(RetailUiRuntime.cs:1907-1908) pushes every one of them through
_bindings.Options.DisplaySystemMessage, which is bound to
d.Communication.AddText(text, RetailLogTextType.ClientLocal)
(InteractionRetainedUiComposition.cs:816-817).
RuntimeCommunicationState.AddText (RuntimeCommunicationState.cs:197-201)
sends ClientLocal (0x1A) to SpewBox.Enqueue and returns — it never
reaches the chat transcript. SpewBoxState.MaxConcurrentItems = 4
(SpewBoxState.cs:77), and Tick drains the whole pending queue in one
call, dropping the oldest on overflow (:132-149). Six lines enqueued in one
click therefore leave four visible: the first two — Camera Stiffness
and Camera Adjustment — are discarded before they are ever drawn. They also
expire after 5 s (:88).
The routing itself is not byte-verified. The commit and register call the
format strings byte-verified (VAs 0x007A8A70 … 0x007A88D0), and the
research doc's §4.4 table lists exactly those VAs — but neither the doc nor
the code records the AddTextToScroll type argument at
0x0049E8F0..0x0049EB81. This project has already been burned on precisely
this inference: ClientTextRefusals.cs:117 documents a site where "the actual
site passes type 0 (Default), not 0x1A". If retail passes 0 here, all
six lines belong in the scrolling chat window, all six are visible, and OP3's
routing is wrong in a way the user will see.
Fix: either byte-verify the type at the call sites and route accordingly, or —
if the sweep is out of scope this slice — file a register row recording the
assumed ClientLocal routing plus the 4-slot truncation, per CLAUDE.md's
"any commit that introduces a deviation adds its register row IN THE SAME
COMMIT". Right now there is neither evidence nor a row. (AD-75 covers the
UA/RA MessageBoxA → interface-text substitution; it says nothing about the
mouse-turning macro's six lines.)
MUST-FIX 2 — the plan assigned this review a register ruling that OP3 did not file
docs/plans/2026-08-10-options-panel-campaign.md:379-382:
The
0x21000017dockedgmPanelUIhost variant — acdream ships the floating host only (register row in OP3 if the review deems it a divergence; retail exposes both).
I deem it a divergence: retail exposes both a floating and a docked
gmPanelUI host and acdream mounts only 0x2100006E, so no gmPanelUI child
— Options included — can be docked. The commit files AD-74/75/76 and TS-74
but no row for this. It is inherited rather than introduced by OP3 (every
pre-existing panel already mounts floating-only), which is exactly why it has
gone unrowed until now, and why the plan routed the ruling here. One AD row,
cited at RetailUiRuntime.MountOptionsPanel / the shared host id, closes it.
SHOULD-FIX 1 — IsGrounded returns false outside player mode, so "Exit to Character Selection" is unusable in fly mode and emits a mid-air message that is not true; the code comment asserts the opposite
InteractionRetainedUiComposition.cs:322-324:
IsGrounded: () => d.PlayerMode.IsPlayerMode
&& d.PlayerController.Controller is { IsAirborne: false },
with the comment (:320-322) claiming "'on the ground' is meaningless
outside player mode, so the mid-air refusal never fires while flying/
spectating." The code does the reverse: outside player mode IsGrounded()
is false, so RequestExitToCharacterSelection's else branch
(RetailUiRuntime.cs:1871-1873) fires CantLogOffMidAir. In fly/chase-camera
mode the button confirms, refuses, and shows a message that is factually
wrong. The underlying expression is the project's established playerOnGround
shape (same three lines at :322-324 feed the item-drop gate), so the shape
is fine — the comment is inverted and the not-in-player-mode case is
unconsidered. Impact is contained (Exit Game right beside it still works, and
retail has no fly mode) but the comment must not survive as-is.
SHOULD-FIX 2 — Options is the only shared-geometry main panel that resizes horizontally and on all four edges; its width is silently discarded whenever a sibling is shown
RetailUiRuntime.cs:1970-1980 gives Options ResizeX = true,
ResizableEdges = Left|Right|Top|Bottom, MinWidth = 300f,
MinHeight = 200f. Every one of its nine shared-rectangle siblings is
ResizeX = false, ResizableEdges = Bottom, with no explicit constraints
(Enumeration A). Consequences, all through
RetailPanelUiController's one _mainPanelGeometry:
- A user widens Options to 450.
OnWindowResized→CaptureAndSynchronize…(:224-247) writes 450 into the shared rect; siblings ignore it (RetailWindowManager.ResizeTo:185forceswidth = frame.Widthwhen!ResizeX). Open the character sheet, then reopen Options:PrepareMainPanelGeometry→ApplyWindowGeometry(:265-282,:306-314) resizes Options back to the sibling's 300. The width is lost. This contradicts the OP3 gate script's own item 5 ("remembers its geometry across a close/reopen"). - The 300/200 minimums are invented, not authored —
DatConstraintSourceis not passed, soResolveConstraint(RetailWindowFrame.cs:160-163) uses the literals. Because all ten panels are authored 300 wide,MinWidth=300currently pins Options to exactly its authored width and no clamp feedback loop can start; that is luck, not design. - The four-edge resize also means Options is the only panel whose top/left drag moves the shared rect origin for every sibling.
Either match the sibling policy (ResizeX = false, bottom-only, drop the
literals) or make the whole shared group consistent — but not one panel with
its own rules inside a single-rectangle group. Needs the connected visual
gate either way; I cannot run the client.
SHOULD-FIX 3 — no test pins the two seams the review was asked about: mutual exclusion against a real existing panel, and the new dialog's queue behavior
The new suites are thorough on the model (OptionPageModelTests, 20 facts)
and the controller's own content (OptionsPanelControllerTests, 15 facts),
and they do pin the F11 route on both sides
(GameplayInputCommandControllerTests.cs:21 + KeyBindingsRetailTests.cs:239).
What is missing:
- Panel mutual exclusion with an existing panel.
RetailPanelCatalogTestsasserts catalog membership only. No test registers, say, panel 11 (character) and panel 10 (options) in aRetailPanelUiControllerand asserts that showing Options hides the character sheet — the exact interaction the slice's headline claim ("shares retail's mutual exclusion for free") rests on. The mechanism is exercised byRetailPanelUiControllerTestswith synthetic ids, so the risk is low, but the claim is unpinned. - The confirmation dialog's queue key.
RequestExitToCharacterSelectiongoes throughShowConfirmation→MakeConfirmationwith the defaultqueueKey = 2(RetailDialogFactory.cs:125-136,:10), the same key every existing confirmation uses — see NOTE 3. No test pins that the new dialog shares the existing key rather than inventing one. - No test asserts
ActivePageChangedstays silent on a dormant Type-8 host (AD-73's contract). See NOTE 2.
Fixture/conformance suites are unaffected: RetailLayoutFixtureGenerator
gains one row (+2 lines), FixtureLoader two new loaders, and no existing
fixture was regenerated (git show --stat shows only the new
options_panel_2100006E_1000018D.json). Existing catalog assertions in
AppraisalUiControllerTests.cs:683-691 and
EffectsUiControllerTests.cs:256-258 are negative assertions about
Examination/Effects and remain true. No count-based assertion over
MountedPanels/ToolbarPanels exists to go stale.
SHOULD-FIX 4 — the Urgent Assistance / Report Abuse bodies are three-line, ~110-character-URL messages delivered into a 4-slot, 5-second, centered transient overlay
OptionsPanelText.UrgentAssistanceUnavailable / ReportAbuseUnavailable
(OptionsPanelText.cs:106-118) embed two \ns and the full
support.turbine.com URL, and go out through the same ClientLocal seam
(OptionsPanelController.cs:171-174). AddText only trims the ends
(RuntimeCommunicationState.cs:195), so the whole three-line body lands as a
single SpewBoxEntry that expires in 5 s. The divergence is registered
(AD-75), but its legibility is not: the user has 5 seconds to transcribe a
110-character URL from a transient overlay. Worth a look at the connected
gate; if it is unreadable, the honest fix is the chat transcript (which
scrolls back), not a longer timeout.
NOTE 1 — the mount-time AfterApply publish is harmless, but it does fire before any session exists
MountOptionsPanel calls controller.ActivateTabs() (RetailUiRuntime.cs:1958)
during Initialize(), which fires ActivePageChanged(0, Gameplay) →
OptionPage.OnShown() → Apply() → AfterApply →
_bindings.Options.CommandBus().Publish(new SaveCharacterOptionsRuntimeCmd())
(:1945-1946). DeferredLiveSessionUiAuthority.Commands returns
NullCommandBus.Instance when unbound (InteractionUiRuntimeSources.cs:222-231),
so there is no NRE and no wire traffic at mount. Post-connect, the same
publish fires on every window show and every tab switch; the router's
SaveCharacterOptionsIfDirty is dirty-gated (LiveSessionRuntimeFactory.cs:359-372),
so it is a no-op unless the batched module is actually dirty. Retail-cited
(PlayerOptionPage::SaveCurrentValues @0x004F2710's SaveToServer(0) tail).
No action needed — recorded because "opening a UI panel sends a character-
options save" is the kind of thing a future reader will flag.
NOTE 2 — ActivePageChanged cannot fire during dormant operation, but only because nobody calls SwitchTo
The event is raised from exactly one place: the tail of SwitchTo
(UiTabPanel.cs:212-213), after ActivePageElementId is updated (so handlers
read the new value — which OptionsPanelController.OnHidden at :217 relies
on). SwitchTo's only callers are ActivateTabBehavior's default switch
(:186) and the click handlers that method registers (:174). Dormant
Type-8 hosts (vendor 0x100000B8, character-sheet root 0x10000227,
spellbook root 0x100002A8, combat 0x100000A2 — AD-73) never run either,
so the event never fires for them. But SwitchTo is public and its own
doc (:193-195) invites direct calls — a future controller calling
SwitchTo on a dormant host WOULD raise it. Since the only subscriber is
OptionsPanelController on its own instance (:131), and it unsubscribes in
Dispose (:233), there is no existing consumer to affect today. Existing
UiTabPanel consumers: none subscribe; the added field is 8 bytes/instance.
NOTE 3 — the new dialog respects the OpenNextDialog duplicate-key hazard because it does not introduce a key
RequestExitToCharacterSelection reuses ShowConfirmation
(RetailUiRuntime.cs:534-541) → MakeConfirmation with the default
queueKey = DefaultQueueKey = 2 (RetailDialogFactory.cs:10, :128). Every
confirmation in the codebase uses that same key (only one QueueKey constant
exists outside the factory: RetailDialogData.cs:19, the property id). The
chat digest's DO-NOT-RETRY hazard is the Dictionary.Add duplicate at
OpenNextDialog; the existing guard at RetailDialogFactory.cs:332-333
(if (_activeQueued.ContainsKey(queueKey)) return;) already covers the
re-entrant case, and OP3 adds no new key for it to miss. Correct by
construction. Behavioral consequence: clicking Exit-to-char-select while a
server-driven confirmation is up queues behind it, same as every other
client-side confirmation.
NOTE 4 — the Exit-Game / Escape / indicator-bar exit paths are genuinely shared and genuinely unchanged
ExitGame: _bindings.Indicators.EndCharacterSession (RetailUiRuntime.cs:1942)
resolves to d.Window.Close (InteractionRetainedUiComposition.cs:688) — the
same delegate behind (a) the indicator bar's end-session button, which wraps
it in RequestEndCharacterSession's confirmation (:1844-1851), and (b) the
Escape key's terminal branch, via a separate GameplayWindowCommands(d.Window.Close)
instance (SessionPlayerComposition.cs:1200 → GameplayInputCommandController.cs:269).
OP3 adds a third caller and modifies neither existing one. The refactor that
extracted ResolveEndCharacterSessionConfirmMessage (:1832-1841) is
behavior-preserving for RequestEndCharacterSession — same DAT id
0x23000001, same hash, same fallback string, same DatLock scope.
Retail-faithfulness of "Exit Game has no confirmation" is confirmed by the
research doc's RecvNotice_EndCharacterSession(0) branch
(2026-08-10-keyboard-config-and-gameplay-tab.md:151-154): param 0 sets
m_shouldQuitOnLogout = 1 and skips both the dialog and the mid-air check —
which is also why OP3 correctly applies the mid-air refusal to
Exit-to-char-select only.
NOTE 5 — settings-file schema and versioning are safe; the concurrent-write hazard is pre-existing and unchanged in kind
SaveCameraTurning goes through the existing SaveSection
(SettingsStore.cs:250-252, :658-702), which preserves every non-target
top-level key as raw text (:663-683) and re-stamps version: 2
unchanged (CurrentSchemaVersion at :37 is untouched). cameraTurning is
a new top-level key, so: older acdream builds ignore it on read and preserve
it on write; newer builds fall back to CameraTurningSettings.Default
per-field when the section or a key is missing (:224-241). No schema break
for existing settings.json readers.
Concurrency: SaveSection is a non-atomic read-modify-File.WriteAllText,
and SaveWindowLayout is a separate LoadMutableRoot/WriteMutableRoot
path over the same file (:435-462). Interleaving either pair can clobber.
That hazard predates OP3; OP3 adds one more writer to the SaveSection side,
driven from a UI click handler on the same thread as the existing settings
saves. No new thread is introduced. Recorded, not charged to this slice.
NOTE 6 — LoadCameraTurning/SaveCameraTurning correctly bypass the cached-snapshot pattern, and the interface widening is fully covered
RuntimeSettingsController gains two pass-throughs
(RuntimeSettingsController.cs:444-467) that read/write storage directly
rather than through the RuntimeSettingsSnapshot cache the
Display/Gameplay/Chat sections use. The implementer flagged this as
deliberate; it is correct here — a cached, change-notified copy exists to
drive live UI and there is no Camera/Input UI surface until OP6. The widened
IRuntimeSettingsStorage has exactly two implementers
(JsonRuntimeSettingsStorage at :42, the test fake at
RuntimeSettingsControllerTests.cs:1280), both updated. Save failures are
logged not thrown, matching every other Set* in the class.
NOTE 7 — the five persisted camera preferences are a second, disconnected store for a value acdream already owns
cameraTurning.mouseLookSensitivity is persisted but never read by the
camera; the live sensitivity acdream actually uses lives in
CameraPointerInputController (_chase.Sensitivity, _flySensitivity,
_orbitSensitivity at :110-111, :174-182) and is adjusted by the
AcdreamSensitivityUp/Down actions. TS-74 mentions "(unwired) preferences"
in passing, but only in the context of the UseMouseTurning bit; the
five-preference store-without-consumer is not itself rowed. Low risk today
(nothing reads it), but OP6 will surface these in the Config tab, at which
point two stores for one concept becomes user-visible. Worth a sentence in
TS-74 rather than a new row.
Related: SetSingleCharacterOptionRuntimeCmd publishes directly on the bus
(RetailUiRuntime.cs:1900-1903), matching the established CH3 pattern at
RuntimeSettingsTargets.cs:321-322 including its documented silent-drop-
while-disconnected behavior. The macro's own wrinkle: the six chat lines are
emitted unconditionally, so a drop while disconnected still prints "Turn to
Face Camera was changed from FALSE … to TRUE" when neither the wire send nor
the local bit write happened (SendIfActive skips the whole lambda, so
TrySetOption never runs). The Options panel is only reachable in-world, so
the window is narrow.
NOTE 8 — IDevToolsGameplayCommands.ToggleSettingsPanel is now fully orphaned
Zero production callers remain (Enumeration C). The commit comment at
GameplayInputCommandController.cs:242-244 describes it as having "no-op
wiring elsewhere"; there is none. Slated for OP9 per the same comment —
worth confirming it actually appears on OP9's list rather than relying on the
comment.
Minor inconsistency in the same area: OptionsPanelController.Bind logs a
loud warning when any of the seven Gameplay buttons fails to resolve
(BindButton, :188-197) but the close button (:160-161) fails silently
if 0x10000210 ever stops building as a UiButton — a panel you cannot
close by its X, with no log. One line to make it symmetric.
Verdict
APPROVE-WITH-FIXES.
The structural bet — register through RetailPanelCatalog +
RetailPanelUiController + the existing generic toolbar binder, rather than
building an Options-specific path — is the right one and is why the blast
radius on already-shipped panels is as small as it is. Both axes walk clean
on the mechanisms: exactly one toolbar button changed state, no other panel's
exclusion/stacking/deferred-restore behavior moved, the dialog queue key is
reused rather than invented, the settings schema is additive and
version-neutral, F11's binding predates the slice and its old target was a
proven no-op, and the new UiTabPanel event cannot fire on a dormant host.
The two MUST-FIX items are an unverified chat-routing decision with a
demonstrable line-loss consequence, and a register row the campaign plan
explicitly assigned to this review. Neither blocks the mechanism; both must
land before the connected gate is meaningful, since the first one changes
what the gate is supposed to observe.