fix(ui): OP6 rework — six range captions, un-invert Sound enabled flags, five font faces

Fixes all three MUST-FIX findings from the OP6 REJECT review
(docs/research/2026-08-11-op6-review.md) plus its SHOULD-FIXes and NOTEs.

M1 — the "retail ships zero range captions" claim was a Binary Ninja
constant-folding artifact (the same class the header-string globals a few
lines above already worked around). The six SetSliderLabel call sites
byte-decode to reads of runtime-filled ID_Graphics_Value_* globals, not
immediate zeros (PE-byte-verified against the PDB-paired acclient.exe,
independently re-derived in this session, not just re-asserted from the
review). ConfigOptionsPageController.BuildSliderRow gained optional
rangeLowKey/rangeHighKey parameters wired for all six idx6 sliders (Camera
Stiffness Soft/Hard, Adjustment Speed Slow/Fast, FOV Narrow/Wide, Screen
Brightness Dark/Bright, Graphics Performance Speed/Detail, Degrade Distance
Close/Far) via the same SetRangeLabel mechanism OP5's Chat opacity sliders
already established. Mouse Look Sensitivity (idx3) correctly stays
uncaptioned — the one genuine SetSliderLabel omission. Class doc corrected;
gate-script lines 535/653-equivalent corrected in place.

M2 — the three Sound "Disabled" toggles were semantically inverted:
SoundManager::effect_sounds_enabled/ambient_sounds_enabled/
interface_sounds_enabled are all compiled = 1 in .data, and
UserPreferences::RegisterPreference binds the checkbox's boolean value
DIRECTLY onto those enabled-sense statics — checked-by-default means
enabled-by-default, not disabled. AudioSettings.SfxDisabled/AmbientDisabled/
InterfaceDisabled renamed to SfxEnabled/AmbientEnabled/InterfaceEnabled
(fresh JSON keys — the rejected slice's keys never shipped in an accepted
build); RuntimeSettingsStartupTargets.ApplyAudio now computes effective
volume through the extracted, independently-unit-tested pure function
ComputeEffectiveCategoryVolumes (enabled ? slider : 0f). This closes the
blast radius the review flagged: a missing key in an EXISTING settings.json
now falls back to AudioSettings.Default, which is enabled=true, so a fresh
launch is audible, not muted. AP-199's wording and gate-script step 6
corrected; the enshrined-inversion test rewritten to assert the correct
default and a new SettingsStore test pins the legacy-file fallback path.

M3 — UI_ChatFontFace now ships all five of retail's authored choices
(Arial, CourierNew, PalatinoLinotype, Tahoma, TimesNewRoman — a fixed
compile-time array at gmClient::InitUIPreferences, PE-byte-verified
present verbatim in .rdata, not a per-machine runtime enumeration as the
rejected slice's comment claimed). Default index 2 (PalatinoLinotype) now
indexes a real entry.

S1 — Bind() now emits the sixth trailing AddSeperator retail's own
InitOptions ends with (0x0049E80D), matching retail's 39-item ListBox (6
headers + 6 separators + 27 option-widget-rows) instead of 38.

S2 — Screen Brightness gets its own DisplaySettings.ScreenBrightness field
([-1,1], default 0) instead of overloading Gamma, which has a different
unit system (default 1.0, legacy [0.5,2.0] slider) and its own live
Settings-panel consumer.

S3 — UiScrollbar and UiMenu gained a settable TooltipText surfaced through
GetTooltipText (UiButton's existing pattern). Every slider and menu row's
own interactive widget (not just toggle/trio rows) now carries retail's
"<label>_Help" tooltip, verified as a universal suffix convention across
every AttachPreference site touched by this tab.

S4 — "800x600" added to DisplaySettings.AvailableResolutions: a genuine
retail display mode (Device::ForceDisplayResolution(1,0x320,0x258) at
startup) and the Config tab's own byte-verified Resolution row default, not
an invented preset. Defaults now lands on a highlighted, re-selectable
dropdown entry instead of an orphaned value.

S5 — four new/extended tests: ComputeEffectiveCategoryVolumes gets a
dedicated pure-function value assertion (Theory + a default-profile-is-
audible Fact) in RuntimeSettingsControllerTests, closing the "only event
order was asserted" gap that let M2 ship; a label/choice-key conformance
table in ConfigOptionsPageControllerTests enumerates every key this tab
queries (traced directly from the fixed code paths, not guessed) and fails
on an invented OR a dropped key; a per-row DefaultValue pin asserts every
row's default against the retail literal directly, independent of the
underlying settings-record defaults; and the S1 separator fix gets its own
39-item stacked-ListBox count pin.

NOTEs — AP-198's row count was always ten (its own enumeration never said
nine); the commit-message inconsistency N1 flagged is reconciled in both
the row and the section-summary line, and its Screen Brightness sub-clause
now matches S2. N2: Bind() now reads the scrollbar id from
UiTemplateListBox.ScrollbarElementId (dat property 0x72) instead of a
hardcoded constant. N3 (batch Defaults writes) and N4 (AfterApply on
Config-tab entry, needs no action) are left as recorded — out of this
rework's scope per the review's own disposition.

Full Release suite: 13,125 passed / 4 skipped / 0 failed (baseline
13,117/4/0 — net +8 tests added, 0 regressions, 0 removed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-11 08:06:20 +02:00
parent 4996974cfd
commit 472525b99e
11 changed files with 774 additions and 168 deletions

View file

@ -21,9 +21,9 @@ public enum ParticleRange
/// <c>gmClient::InitUIPreferences @0x004035b0</c> DOES register
/// <c>Render_FieldOfView</c> (<c>ID_Graphics_FieldOfView</c>, range
/// [10,160]), <c>Display_SyncToRefresh</c>, <c>Display_Resolution</c>, and
/// <c>Render_ScreenBrightness</c> (range [-1,1], mapped onto
/// <see cref="Gamma"/> below) as genuine <c>UserPreferences.ini</c> rows —
/// they simply had no acdream UI surface until OP6's Config tab. Resolution/
/// <c>Render_ScreenBrightness</c> (<see cref="ScreenBrightness"/> below,
/// range [-1,1]) as genuine <c>UserPreferences.ini</c> rows — they simply
/// had no acdream UI surface until OP6's Config tab. Resolution/
/// Fullscreen are LIVE on save (<c>RuntimeSettingsTargets.
/// ApplyDisplayWindowState</c> resizes the window immediately); VSync/FOV/
/// Gamma apply at the next launch only (<c>RuntimeSettingsController.
@ -51,6 +51,18 @@ public sealed record DisplaySettings(
// per-feature knobs (register row, OP6). Persisted faithfully; every
// default below is retail's own byte-verified
// gmClient::InitUIPreferences / gmConfigUI::InitOptions literal.
//
// OP6 rework (2026-08-11, review S2): Screen Brightness gets its OWN
// field — the rejected slice reused Gamma (a pre-existing multiplier,
// default 1.0, legacy Settings-panel range [0.5, 2.0]), a genuinely
// different unit system from retail's own Render_ScreenBrightness
// range [-1, 1] / default 0 (AttachPreference @0x004043df,
// SetPreferenceRange @0x004043f3). Overloading Gamma pinned the Config
// row at the WRONG default (a fresh Gamma=1.0 normalizes to the
// slider's maximum, not center) and made Defaults write a value (0)
// outside the legacy slider's own range. Gamma itself is untouched —
// still the pre-existing multiplier the legacy Settings panel drives.
float ScreenBrightness = 0f,
bool AutomaticDegrades = false,
float GraphicsPerformance = 0f,
float DegradeDistance = 50f,
@ -81,9 +93,24 @@ public sealed record DisplaySettings(
Quality: QualityPreset.High,
ParticleRange: ParticleRange.Extended);
/// <summary>16:9 resolution presets offered in the dropdown.</summary>
/// <summary>
/// Resolution presets offered in the dropdown. <c>800x600</c> is retail's
/// OWN Config-tab default (OP6 rework, review S4) — a genuine legacy
/// display mode, not an invented entry: <c>gmClient::Init @0x004047af</c>
/// calls <c>Device::ForceDisplayResolution(1, 0x320, 0x258)</c> (0x320 =
/// 800, 0x258 = 600) at startup, and <c>gmConfigUI::InitOptions</c>'s own
/// <c>SetDefaultValue(0x03200258)</c> (byte-verified) names it as the
/// Resolution row's default. Without it in this list, clicking Defaults
/// resized the window correctly but left the dropdown showing an entry
/// that could never be re-selected — the same "opaque default" shape
/// LandscapeDrawDistance has for a genuinely different reason (AP-198's
/// sub-note); this one has a one-line fix instead of an opaque default,
/// so it gets the fix. The rest of the list is acdream's own modern
/// 16:9 preset ladder, not retail-authored.
/// </summary>
public static IReadOnlyList<string> AvailableResolutions { get; } = new[]
{
"800x600",
"1280x720",
"1366x768",
"1600x900",