acdream/docs/research/2026-08-11-op6-rereview.md
Erik ff5776415b docs: OP6 re-review CLOSED — slice code-complete, gate ready; doc residuals
The re-reviewer byte-decoded ALL six caption sites independently (no
transposition; a third evidence line from the fixture's authored
left/right label geometry), re-verified the audio chain through the one
place an inversion could still hide (the checkbox pass-throughs), and
hand-traced the 97-key conformance table (exactly 97, none invented or
dropped). Residuals applied here: SF-1 the AudioSettings doc comment's
wrong function attribution (the SetDefaultValue literals live in
gmConfigUI::InitOptions @0x0049E435/E457/E479, not InitUIPreferences);
SF-2 the gate script no longer asserts an unread DAT caption — it gates
on behaviour and asks the tester to report the authored English
verbatim; lane A's stale '24 option rows' corrected to 27 (the 39-item
pin is the authoritative tally). AudioSettings.cs change is
comment-only (no executable-code delta; suite state carries from
67b0815c, re-verified at the next code commit).

Campaign state: OP1-OP7 ALL CODE-COMPLETE; OP3/OP4/OP5/OP6 gates ready;
OP8 (Configure Keyboard) is the sole remaining implementation slice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 08:21:07 +02:00

24 KiB
Raw Blame History

OP6 rework — focused re-review

Commit under re-review: 472525b9 "fix(ui): OP6 rework — six range captions, un-invert Sound enabled flags, five font faces" Closes: docs/research/2026-08-11-op6-review.md (REJECT — M1/M2/M3, S1S5, N1N4) Reviewer: focused re-reviewer, Opus, 2026-08-11. Read-only, no subagents, no build/test/launch.

Verdict: CLOSED

All three MUST-FIX findings, all five SHOULD-FIXes, and both actionable NOTEs are closed. Every one of the three MUST-FIX closures was independently re-derived from primary sources in this session, not accepted from the review's own dumps — including a fresh PE byte-decode of all six SetSliderLabel call sites (the review decoded them; the implementer byte-read only two and pattern-matched four; this pass decoded all six plus their push order, and added a third, independent line of evidence the earlier two rounds did not have).

Residuals are two documentation-truthfulness items (SHOULD-FIX) and six NOTEs. None is a behaviour defect; none blocks the connected gate.


Evidence discipline for this pass

Byte quotes below were decoded in this session from C:\Users\erikn\Downloads\acclient.exe (4,837,376 bytes), image base 0x400000, .text VA→file offset = VA - 0x400000. Symbol names came from docs/research/named-retail/acclient_2013_pseudo_c.txt. Layout facts came from the committed fixture tests/AcDream.App.Tests/UI/Layout/fixtures/options_2100002B.json.


Per-finding closure

M1 — six range-caption pairs — CLOSED (independently re-verified, no transposition)

ConfigOptionsPageController.BuildSliderRow gained rangeLowKey/rangeHighKey (:777-778), applied at :792-795 through SetRangeLabel (:834-848) onto SliderRangeMinElementId = 0x1000021E / SliderRangeMaxElementId = 0x1000021F (:187-188).

Full six-site byte decode (this session, all six — not two):

Slider pref pushed SetSliderLabel VA arg3 (low) arg4 (high)
Camera_Stiffness 0x0083E5A0 0x0049E4E0 [0x0083E790] [0x0083E794]
Camera_AdjustmentSpeed 0x0083E5A4 0x0049E51D [0x0083E788] [0x0083E78C]
Render_FieldOfView 0x0083E5F4 0x0049E556 [0x0083E780] [0x0083E784]
Render_ScreenBrightness 0x0083E608 0x0049E614 [0x0083E768] [0x0083E76C]
Render_GraphicsPerformance 0x0083E61C 0x0049E65D [0x0083E770] [0x0083E774]
Render_DegradeDistance 0x0083E620 0x0049E68E [0x0083E778] [0x0083E77C]

Arg order is recovered from the push sequence, not assumed. Two encodings appear and both resolve the same way. Stiffness (0x0049E4C9): 8b 0d 94 e7 83 00 / 8b 15 90 e7 83 00 / 51 52 ... 50 — pushed ecx, edx, eax, so eax (slot) is the last push = first stack arg, edx = arg3, ecx = arg4. Screen Brightness (0x0049E602): 8b 0d 68 e7 83 00 / 8b f8 / a1 6c e7 83 00 / 50 51 57 — pushed eax, ecx, edi, so edi (slot) = first arg, ecx = arg3, eax = arg4. Both give arg3 = the numerically lower global = the low caption.

PDB names confirmed at acclient_2013_pseudo_c.txt:1148623-1148634 (0x0083E768 ID_Graphics_Value_Dark0x0083E794 ID_Graphics_Value_Hard), and the same names are built by compute_str_hash at :769790-769878 with the literals present in .rdata at :975858-975896.

Pairing in the shipped code is correct, with zero transposition:

Row rangeLowKey rangeHighKey file:line
Camera Stiffness Soft Hard ConfigOptionsPageController.cs:396
Camera Adjustment Speed Slow Fast :403
Field Of View Narrow Wide :414
Screen Brightness Dark Bright :482
Graphics Performance Speed Detail :494
Degrade Distance Close Far :501

Third, independent evidence line the earlier rounds did not have. The low/high → element assignment could still have been transposed even with the right pairs. The fixture settles it geometrically: in template 0x1000021D, child 0x1000021E is at X=146, W=60, HJustify=0 (left-aligned, left half of the slider track at X=146, W=120) and 0x1000021F is at X=206, W=60, HJustify=2 (right-aligned, right half). Low = left = 0x1000021E. This matches OP5's already-shipped Chat precedent (ChatOptionsPageController.cs:343-344: arg3 Transparent → Min, arg4 Opaque → Max).

Assessment of the implementer's 2-of-6 chain: sound, and now moot. The four pattern-matched sites are individually byte-confirmed above. The chain was safe to begin with because the six globals are a contiguous, PDB-named .data block in a fixed declaration order and each site reads an adjacent (n, n+4) pair — but "safe reasoning" is now replaced by measurement for all six.

Keys resolve by name, no hard-coded English. SetRangeLabel goes through resolveString(StringTableId, DatStringResolver.ComputeHash(key)) and logs + leaves the widget blank on failure (:839-846) — the same rule as every other string on the page.

Doc + gate corrected. Class doc U4 note rewritten (:46-73), gate script §OP6 intro (2026-08-11-campaign-op-test-script.md:531-547) and "What to report" (:670-675) both now name the six pairs and the one genuine exception. Mouse Look Sensitivity correctly stays uncaptioned (ConfigOptionsPageController.cs:594-598, no range keys) — confirmed against AddSliderOption(this, &Input_MouseLookSensitivity, 0) at 0x0049E76B with no following SetSliderLabel.

The SetSliderLabel call count is exactly 6, verified in the decomp (acclient_2013_pseudo_c.txt:169415, 169419, 169422, 169435, 169439, plus 0x0049E68E) — six labelled sliders, seven sliders total.


M2 — Sound toggles un-inverted — CLOSED (chain re-verified end to end)

AudioSettings.SfxEnabled/AmbientEnabled/InterfaceEnabled, all defaulting true (AudioSettings.cs:90-95).

Retail chain, re-verified independently:

  • UserPreferences::RegisterPreference(&SoundManager::effect_sounds_enabled, &Sound_SoundDisabled, …) at :346727; siblings at :346734 / :346741. The backing variable is enabled-sense; the preference id's name is the legacy INI key ("Sound Disabled").
  • .data statics all compiled = 0x1: :1102289 / :1102294 / :1102299.
  • Every consumer read is enabled-sense: :346813, :346838, :346863, :346896, :346930, :346957, :347014, :347041, :347816, :347960, :348116.
  • The one place an inversion could still have hidden — the checkbox widget — was checked and is clean. UIOption_Checkbox::Refresh (0x004868A0) is SetAttribute_Bool(0xe, this->m_current); SetCurrentValue (0x00486970) is SetAttribute_Bool(0xe, arg2); and the UIPreference branch of GetValue (0x00486F60) returns UIPreferences::InqPreferenceValue's out-param directly (0x00487027). No negation anywhere. Checked = stored 1 = audible.
  • SetDefaultValue(1, 0x3f800000) on all three trios, byte-verified at 0x0049E435 / 0x0049E457 / 0x0049E479 (68 00 00 80 3f 6a 01).

Truth table. ComputeEffectiveCategoryVolumes (RuntimeSettingsTargets.cs:161-167) is enabled ? slider : 0f per-category. Pinned by RuntimeSettingsControllerTests.cs:325-350 (Theory: (T,T) pass-through, (F,F)0f/0f, (T,F) → independent gating) and :352-364 (default profile → 1.0f/1.0f).

ApplyAudio pushes the right values. RuntimeSettingsTargets.cs:131-142: engine.MasterVolume = audio.Master, then engine.SfxVolume = sfx / engine.AmbientVolume = ambient from the helper. All three live call sites now share it: startup (RuntimeSettingsController.cs:263), save (:534RuntimeSettingsTargets.cs:334-335), and the per-frame draft preview (WorldRenderFrameBuilder.cs:372).

Existing settings.json loads with sound ON. SettingsStore.cs:131-133 reads the three fresh keys with d.<field> (= true) as the missing-key fallback. Pinned end-to-end by ConfigOptionsPageControllerTests.cs:134-172, which writes a real legacy-shaped file containing only a display section, loads it, and asserts both the record flags AND that ComputeEffectiveCategoryVolumes returns 1.0f/1.0f.

No old-key remnants. A repo-wide grep for SfxDisabled|AmbientDisabled|InterfaceDisabled|sfxDisabled|ambientDisabled|interfaceDisabled returns three hits, all inside explanatory comments (AudioSettings.cs:88, SettingsStore.cs:125-126). No reader, no writer, no test. WriteAudio (SettingsStore.cs:680-690) emits only the new keys.

AP-199 and gate step 6 are now truthful. retail-divergence-register.md:192 opens with the explicit correction and states "a fresh profile is audible, not muted"; gate step 6 (2026-08-11-campaign-op-test-script.md:578-587) now says sound starts ON and that silence on a fresh character IS a defect to report. §"What to report" adds the same as an explicit reportable (:678-680).


M3 — five font faces — CLOSED

ChatFontFaceChoices (ConfigOptionsPageController.cs:636-640) ships Arial, CourierNew, PalatinoLinotype, Tahoma, TimesNewRoman — matching gmClient::InitUIPreferences's fixed five-compute_str_hash array at 0x00403891, 0x004038C8, 0x00403910, 0x00403958, 0x004039A0 (acclient_2013_pseudo_c.txt:2798, 2806, 2820, 2834, 2848), terminated by one SetEnumChoices(&UI_ChatFontFace, …) at 0x004039ED (:2860). All five literals present in .rdata (:948252-948268).

Default index 2 = PalatinoLinotype, in range. Byte-verified: 6a 02 at 0x0049E7E4 inside the AddMenuOption(&UI_ChatFontFace, 1) at 0x0049E7D4. Wired at ConfigOptionsPageController.cs:665 and pinned by ConfigOptionsPageControllerTests.cs:606 (row 28 → 2).

The wrong "per-machine runtime enumeration / SmartArray has no fixed upper bound" comment is gone, replaced by the corrected doc at :619-635 which names the same SmartArray::grow push idiom ChatFontSizeChoices uses.


S1 — trailing separator — CLOSED

Bind emits a sixth BuildSeparatorRow after the UI section (ConfigOptionsPageController.cs:288-294).

Byte-verified six AddSeperator calls, all targeting 0x004F2730: 0x0049E4A9, 0x0049E58A, 0x0049E6A4, 0x0049E750, 0x0049E7C3, and the tailcall e9 1e 3f 05 00 at 0x0049E80D. Six AddHeader calls (0x004F28B0) at 0x0049E40A, 0x0049E4B6, 0x0049E597, 0x0049E6B1, 0x0049E75D, 0x0049E7D0, reading the six header globals 0x0083E7500x0083E764. Twenty-seven option Add* calls, counted site-by-site in the same dump: Sound 5, Camera 4, Graphics 7, Rendering Quality 6, Input 3, UI 2. 6 + 6 + 27 = 39.

Pinned by ConfigOptionsPageControllerTests.cs:381-413 (Assert.Equal(39, viewport.Children.Count)).

Lane A's stale "24 option rows" line was not corrected in 2026-08-10-options-panel-structure.md — the review flagged that as an aside, not a required fix, and the doc's separator count ("5 interior + 1 trailing") was already right. Recorded as NOTE N-g below.


S2 — Screen Brightness gets its own field — CLOSED

DisplaySettings.ScreenBrightness = 0f (DisplaySettings.cs:65), range [-1,1] at the build site (ConfigOptionsPageController.cs:477-482), matching SetPreferenceRange(&Render_ScreenBrightness, -1f, 1f) (0x004043F3) and the byte-verified SetDefaultValue immediate 6a 00 at 0x0049E61A.

Gamma is untouched. Still float Gamma at DisplaySettings.cs:44, Default.Gamma = 1.0f at :91, its existing pin DisplaySettingsTests.cs:26 (Assert.Equal(1.0f, d.Gamma)) unmodified, and its legacy live control renderer.SliderFloat("Gamma", ref gamma, 0.5f, 2.0f) unmodified at SettingsPanel.cs:219-221. No Config row writes Gamma.

Store: SettingsStore.cs:74 (read, fallback d.ScreenBrightness) and :662 (write). Additive only — an existing settings.json without the key loads 0f. Round-trip pinned at ConfigOptionsPageControllerTests.cs:174-204; default pinned at :83 and at the per-row table :590.


S3 — tooltips on sliders and menus — CLOSED

UiScrollbar.TooltipText + GetTooltipText override (UiScrollbar.cs:88-104) and UiMenu.TooltipText + override (UiMenu.cs:37-48), both following UiButton.cs:92-100 and the existing UiElement.cs:480 virtual hook.

Coverage is now complete for all 30 registered rows:

  • 9 plain toggles + 3 trio toggles → ApplyLabelAndTooltip (:735, :894, :1081-1095).
  • 7 sliders → slider.TooltipText (:805-809).
  • 3 trio slider halves → slider.TooltipText from an explicit sliderTooltipKey (:911-913).
  • 7 int menus + Resolution → menu.TooltipText (:965-969, :1047-1051).

_Help suffix convention spot-checked at 3+ rows against gmClient::InitUIPreferences (and, in fact, at every site in the function): ID_Camera_Stiffness_Help @ 0x00404624AttachPreference(&Camera_Stiffness, …) @ 0x00404647; ID_Graphics_ScreenBrightness_Help @ 0x004043BC@0x004043DF; ID_UI_ChatFontFace_Help @ 0x00403858@0x0040387B. The trio slider halves' keys are also right: ID_Sound_EffectVolume_Help @ 0x004035E7 (pref Sound_SoundVolume), ID_Sound_AmbientVolume_Help @ 0x00403653, ID_Sound_InterfaceVolume_Help @ 0x004036BD — exactly the three sliderTooltipKey values at ConfigOptionsPageController.cs:335, 345, 359. No AttachPreference site in this function deviates from the suffix.

The class doc's "applied uniformly" claim (:33-42) is now TRUE. The narrower scope it states — "every toggle/trio/slider/menu row's own interactive widget", not the sibling name text — is retail-shaped; see NOTE N-e.


S4 — 800x600 selectable — CLOSED

DisplaySettings.AvailableResolutions now leads with "800x600" (DisplaySettings.cs:113). Row default "800x600" at ConfigOptionsPageController.cs:458, byte-verified 68 58 02 20 03 (0x03200258) at 0x0049E5AC.

Blast checked and clean. The only other consumer, SettingsPanel.cs:200-206, resolves by VALUE (Array.IndexOf) and writes back the string, never a persisted index — so prepending an entry shifts no stored data. SettingsPanelTests.cs:334-345 asserts membership only. DisplaySettingsTests.cs:31-45 asserts ascending width; 800 ≤ 1280 holds. See NOTE N-b for the now-stale test name.


S5 — the four missing tests — CLOSED

  1. Effective-engine-volume mappingRuntimeSettingsControllerTests.cs:325-364, Theory + default-audible Fact. Closes the exact gap M2 slipped through.
  2. Label/choice-key conformance tableConfigOptionsPageControllerTests.cs:627-733. Spot-checked: the table is 97 keys with zero duplicates, and I traced every one of the controller's query sites by hand against it — no invented key, no dropped key. Ten verified against the decomp: ID_Sound_SoundFeatures(+_Help) @0x0040371D/@0x004036FA; ID_Sound_DisableSound(+_Help) @0x004035DD/@0x004035BA; ID_Sound_NoFocusNoSound(+_Help) @0x004037E4/@0x004037C1; ID_Camera_AdjustmentSpeed(+_Help) @0x00404688; ID_Camera_AlignToSlope(+_Help) @0x0040461A; ID_Graphics_AdaptiveDegradeBias(+_Help) @0x0040444D; ID_Rendering_SyncToDisplayRefresh(+_Help) @0x004044F8; ID_Rendering_DisplayResolution(+_Help) @0x00404525; ID_Input_UseMouseTurning(+_Help) @0x004045ED; ID_Graphics_LandscapeDrawDistance + its six choices (VeryLow..Extreme) @0x004041B7/@0x00404371. Also confirmed the asymmetric-but-correct entries: the three volume sliders contribute only their _Help key (the trio row has no slider name text), and Resolution's menu items are literal strings, not string-table keys — both match the code paths exactly. The test is load-bearing beyond keys: SetRangeLabel returns early without querying when the caption child is absent, so a missing 0x1000021E/0x1000021F would surface as a dropped ID_Graphics_Value_* key. It is therefore a real structural pin on M1's fix.
  3. Per-row retail-default pinConfigOptionsPageControllerTests.cs:557-625, all 30 rows against the literals. I re-derived every one of the 27 SetDefaultValue immediates from the binary; the table matches (0x3EE66666=0.45, 0x42200000=40, 0x42B40000=90, 0x03200258, 0x42480000=50, 0x3F0CCCCD=0.55, and the menu immediates 6a 02/6a 01/6a 01/6a 08/6a 02/6a 01).
  4. Separator/list-item count pin:381-413 (39).

N1 — AP-198 row count — CLOSED

Section-3 header (retail-divergence-register.md:178) now reads "the Config tab's TEN Graphics/Rendering-Quality-family rows (including Screen Brightness, its own field as of the S2 fix)"; the row body (:191) states the correction and enumerates the same ten. Internally consistent.

N2 — authored scrollbar id — CLOSED

Bind reads listBox.ScrollbarElementId (ConfigOptionsPageController.cs:259), fed from dat property 0x72 via ElementReader.cs:524DatWidgetFactory.cs:106UiTemplateListBox.cs:102/141. The fixture's Config ListBox carries property 114 (0x72) = 268435969 = 0x10000201, so the authored read returns the same value the constant documented. The scrollbarElementId == 0 guard (:261) degrades to the existing log path.

N3 / N4 — recorded, out of scope

Left as the review dispositioned them. N4 needs no action.


Residuals

SHOULD-FIX

SF-1 — AudioSettings.cs:106 misattributes SetDefaultValue to the wrong retail function, contradicting line 50 of the same file. Default's doc says the six trailing fields come from "retail's OWN byte-verified gmClient::InitUIPreferences literals: SetDefaultValue(1, 0x3f800000)". SetDefaultValue does not exist anywhere in gmClient::InitUIPreferences — that function contains only AttachPreference / SetPreferenceRange / SetEnumChoices (verified across its whole body). The SetDefaultValue(1, 0x3f800000) calls are in gmConfigUI::InitOptions at 0x0049E435 / 0x0049E457 / 0x0049E479, which is exactly what the class doc says two paragraphs earlier at AudioSettings.cs:50. The rework edited this sentence (it changed "toggle=checked/'Disabled'=true" to "toggle=checked=enabled") and left the wrong function name in place. One-word fix; matters because this project treats decomp citations as load-bearing.

SF-2 — the gate script asserts an unread DAT caption and tells the tester a checked "Disable Sound Effects" box means audible. 2026-08-11-campaign-op-test-script.md:568-575 and :580-587 name the control by the English "Disable Sound Effects" and instruct that CHECKED = audible. The behaviour is correct (verified above, including the un-inverted checkbox widget). But the review's own M2 fix direction asked to "read the DAT caption for ID_Sound_DisableSound at the gate to confirm which way the LED reads", and the rework did not — it asserted instead that the caption is a legacy misnomer (AudioSettings.cs:41-46). Two failure modes: if the DAT English really does read "Disable Sound Effects", a tester reading step 4 will see a self-contradictory instruction and may report a false defect; if it reads something else, step 4 names a control that is not on screen. Cheapest honest fix: have the step ask the tester to report the checkbox's caption verbatim and describe the observed behaviour, rather than asserting the caption. (Register/AP-199 wording is fine — it describes the stored semantic, not the English.)

NOTE

N-a — ScreenBrightness was inserted mid-record, not appended. DisplaySettings.cs:65 sits between ParticleRange and AutomaticDegrades, shifting every subsequent positional parameter. Safe today: both new DisplaySettings( sites use named arguments (SettingsStore.cs:64, SettingsStoreTests.cs:41) and a positional call would fail to compile on the float-vs-bool mismatch anyway. Appending to the all-defaulted tail is the lower-risk convention for this record family.

N-b — DisplaySettingsTests.AvailableResolutions_includes_common_16_9_options is now misnamed. DisplaySettingsTests.cs:31 — the list it validates now contains a 4:3 mode. The assertions still pass (membership + ascending width); only the name and the summary comment are stale.

N-c — ConfigOptionsPageController.ScrollbarElementId is no longer read by production code. After N2, Bind uses the authored field (:259); the constant (:163) survives as documentation and is used only by the two scrollbar tests (ConfigOptionsPageControllerTests.cs:767, :812). Those tests therefore assert the constant, not the authored value — a DAT change would fail them loudly rather than silently, which is the acceptable direction.

N-d — the engine assignment itself is still untested. ComputeEffectiveCategoryVolumes is pinned three ways, but engine.SfxVolume = sfx; engine.AmbientVolume = ambient (RuntimeSettingsTargets.cs:140-141) has no fake-engine test — swapping those two lines would pass every suite. The FakeRuntimeTargets "target-audio" event assertion (RuntimeSettingsControllerTests.cs:301) still only proves the call happened. Low risk (two adjacent lines, no branch), recorded so it is not rediscovered.

N-e — tooltips are on the interactive widget only, and that is retail-shaped. Do not reopen. SetLabelText (:1109-1120) still sets no tooltip on the sibling name UiText. Retail behaves the same: PlayerOptionPage::AddSliderOption/AddMenuOption return the option element and UIOption_*::SetTooltip (0x00487053UIElement::SetTooltip) targets it; SetSliderLabel only writes StringInfo onto the text children. acdream matches. Recorded so a later reviewer does not re-file S3.

N-f — the "counterintuitive default" framing is fully gone from the gate. The old step-6 text that told the user muted-by-default was "not a bug" has been replaced by its inverse, including an explicit "if you hear silence… that IS a defect — report it" (:587). No surviving gate step blesses the M2 behaviour.

N-g — lane A's "24 option rows" is still stale. 2026-08-10-options-panel-structure.md:487 still says 24; the decomp has 27, which is what the slice implements and what the new 39-item pin encodes. The review listed this as a parenthetical aside inside S1, not a required fix, and it was not corrected. Harmless (nothing reads it), but it is the kind of stale research line that costs a future session an hour.


Blast radius (new surfaces this rework introduced)

Surface Consumers found Verdict
AudioSettings field renames RuntimeSettingsTargets.cs:164-165; ConfigOptionsPageController.cs:339/340/349/350/363/364; SettingsStore.cs:131-133, 683-689; 3 test files Complete. Zero readers of the old names; renames are compile-breaking, so nothing can be silently missed.
Startup path RuntimeSettingsController.cs:263RuntimeSettingsStartupTargets.ApplyAudio Correct values via the shared helper.
Dev-tools / legacy panel SettingsPanel.cs has no audio-enable surface; SettingsVM mutates by with Untouched; draft preview (WorldRenderFrameBuilder.cs:372) uses the same corrected helper.
OP7 / headless bots src/AcDream.Headless/ — zero Audio references Confirmed: bots declare no audio; OP7's characterOptions block is unaffected.
UiScrollbar/UiMenu.TooltipText Default nullGetTooltipText() returns null (UiScrollbar.cs:103, UiMenu.cs:48) Contained. Every pre-existing scrollbar/menu (Chat opacity sliders, inventory, etc.) is behaviourally unchanged.
DisplaySettings.ScreenBrightness SettingsStore.cs:74, 662; ConfigOptionsPageController.cs:480-481 Additive-only; Gamma and its legacy [0.5,2.0] control untouched, its existing default pin unmodified.
AvailableResolutions prepend SettingsPanel.cs:200-206 (value-based IndexOf) No persisted index anywhere; no shift hazard.

Fix checklist

# Severity Item
SF-1 SHOULD AudioSettings.cs:106 — cite gmConfigUI::InitOptions (@0x0049E435/E457/E479) for SetDefaultValue, not gmClient::InitUIPreferences; line 50 of the same file already has it right
SF-2 SHOULD Gate script :568-587 — ask the tester to report the Sound-trio checkbox caption verbatim instead of asserting "Disable Sound Effects"; the DAT string for ID_Sound_DisableSound was never read
N-a…N-g NOTE Mid-record insert; stale 16_9 test name; now-unused ScrollbarElementId constant; untested engine assignment; tooltip scope is retail-shaped (do not reopen); gate no longer blesses M2; lane A's stale "24 option rows"