feat(ui): Campaign OP slice OP6 — the Config tab

Binds the retail Options panel's Config tab (LayoutDesc 0x21000029, 27
authored rows across 6 sections) through OP2's template mechanism and
OP3's per-page OptionPage model, matching the Character/Chat tab
controllers' established pattern.

The row table is transcribed directly from two decompiled sources —
gmConfigUI::InitOptions @0x0049E400 (row order, widget shape, defaults)
and gmClient::InitUIPreferences @0x004035b0 (the complete
UIPreferences::AttachPreference registration: every label/tooltip key,
every slider's real-unit range, every menu's enum choices) — which
resolves the research docs' own "U4" unverified slider-caption pairing:
retail ships ZERO range captions on this tab (every SetSliderLabel call
passes literal string id 0).

Consumer disposition: LIVE — Sound/Ambient volume-trio sliders and their
toggle halves (AudioSettings.SfxDisabled/AmbientDisabled now gate the
already-live engine write; RuntimeSettingsController.SaveAudio newly
pushes into OpenAlAudioEngine on every change, not just at startup),
Resolution/Full Screen (immediate window resize on save). NEXT-LAUNCH
(pre-existing precedent): Sync To Refresh, Field of View. STORE-ONLY
(register rows AP-198/199/200, TS-74 extended): Sound Features/Interface
trio/Play-Only-When-Active, the nine Graphics/Rendering-Quality rows
(Vulkan has no per-feature render knobs), Camera/Input's six rows and
Use Mouse Turning (no persistent mouse-turning camera mode), Chat Font
Face/Size (distinct new fields from the existing live ChatSettings.FontSize).

AudioSettings/DisplaySettings/CameraTurningSettings/ChatSettings each
gain new fields for their slice of the 27 rows, backed by SettingsStore
round-trips. A real bug caught by testing: the scrollbar scope lookup
used the standalone-layout root id (0x100001FF), which does not survive
base-merge into the host-mounted tree — fixed to scope from the tab
host's own page-slot id (0x10000213), matching Chat's established
pattern for the same shared-scrollbar-id hazard (0x10000201, authored by
both the Chat and Config ListBoxes).

30 new tests (27 authored rows register as 30 IOptionRow instances — the
three toggle+slider trios each register two). Full Release suite:
13,107 passed / 4 skipped / 0 failed (was 13,083/4/0 — net +24, the one
existing RuntimeSettingsControllerTests case updated for SaveAudio's new
live-apply call, not a regression).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-11 07:16:35 +02:00
parent 527a3a4056
commit f5ac1742ba
16 changed files with 2188 additions and 30 deletions

View file

@ -129,6 +129,17 @@ internal interface IRuntimeSettingsTargets
{
void ApplyDisplayWindowState(DisplaySettings display);
/// <summary>
/// Campaign OP slice OP6 (2026-08-11): pushes the CURRENT audio
/// snapshot into the live <c>OpenAlAudioEngine</c> — the SAME
/// <c>RuntimeSettingsStartupTargets.ApplyAudio</c> static helper the
/// startup path already used, now also reachable on every
/// <c>SaveAudio</c>. Before this, an Audio-tab change never took
/// effect until the next process launch; the mechanism already
/// existed, it was just never invoked outside startup.
/// </summary>
void ApplyAudio(AudioSettings audio);
void ApplyQuality(QualitySettings quality);
void ApplyUiLock(bool locked);
@ -485,7 +496,13 @@ internal sealed class RuntimeSettingsController :
_runtimeTargets?.ApplyQuality(resolved);
}
private void SaveDisplay(DisplaySettings display)
/// <summary>Widened from private to public at Campaign OP slice OP6 —
/// same shape as the already-public <see cref="SaveCameraTurning"/>,
/// now also called directly by <c>ConfigOptionsPageController</c>'s
/// Display-backed rows (resolution/fullscreen/vsync/FOV/gamma/quality
/// family) rather than only through the (soon-retired) SettingsVM
/// callback wiring.</summary>
public void SaveDisplay(DisplaySettings display)
{
try
{
@ -501,12 +518,20 @@ internal sealed class RuntimeSettingsController :
}
}
private void SaveAudio(AudioSettings audio)
/// <summary>Widened from private to public at Campaign OP slice OP6,
/// same reason as <see cref="SaveDisplay"/>. Also now pushes the saved
/// snapshot into the live engine via
/// <see cref="IRuntimeSettingsTargets.ApplyAudio"/> — previously this
/// method only persisted; Audio-tab changes took effect on the NEXT
/// launch only. OP6's Sound-trio sliders/toggles are the first live
/// consumer.</summary>
public void SaveAudio(AudioSettings audio)
{
try
{
_storage.SaveAudio(audio);
Audio = audio;
_runtimeTargets?.ApplyAudio(audio);
_log($"settings: audio saved to {_storage.Location}");
}
catch (Exception ex)
@ -533,7 +558,12 @@ internal sealed class RuntimeSettingsController :
}
}
private void SaveChat(ChatSettings chat)
/// <summary>Widened from private to public at Campaign OP slice OP6,
/// same reason as <see cref="SaveDisplay"/> — the Config tab's Chat
/// Font Face/Size rows (store-only, no diff against the five wired
/// Hear*Chat fields) reuse this exact seam rather than a parallel
/// save path.</summary>
public void SaveChat(ChatSettings chat)
{
ChatSettings previous = Chat;
try