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:
parent
527a3a4056
commit
f5ac1742ba
16 changed files with 2188 additions and 30 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -136,8 +136,16 @@ internal sealed class RuntimeSettingsStartupTargets : IRuntimeSettingsStartupTar
|
|||
if (engine is not { IsAvailable: true })
|
||||
return;
|
||||
engine.MasterVolume = audio.Master;
|
||||
engine.SfxVolume = audio.Sfx;
|
||||
engine.AmbientVolume = audio.Ambient;
|
||||
// Campaign OP slice OP6: the Config tab's toggle halves of the
|
||||
// Sound/Ambient volume trios (Sound_SoundDisabled/
|
||||
// Sound_AmbientSoundDisabled) gate the SAME slider value — retail's
|
||||
// UIOption_CheckboxSlider is one row over two preferences, and a
|
||||
// checked "Disable Sound Effects" LED mutes the category regardless
|
||||
// of what the slider itself is set to. No engine-level "disabled"
|
||||
// concept is needed: the effective volume sent IS zero when
|
||||
// disabled, exactly as if the user dragged the slider to zero.
|
||||
engine.SfxVolume = audio.SfxDisabled ? 0f : audio.Sfx;
|
||||
engine.AmbientVolume = audio.AmbientDisabled ? 0f : audio.Ambient;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -240,6 +248,7 @@ internal sealed class RuntimeSettingsTargets : IRuntimeSettingsTargets
|
|||
private readonly IRuntimeChatOpacityTarget _chatOpacity;
|
||||
private readonly ICommandBus _commands;
|
||||
private readonly Action<string> _log;
|
||||
private readonly OpenAlAudioEngine? _audio;
|
||||
|
||||
public RuntimeSettingsTargets(
|
||||
IRuntimeDisplayWindowTarget displayWindow,
|
||||
|
|
@ -250,7 +259,12 @@ internal sealed class RuntimeSettingsTargets : IRuntimeSettingsTargets
|
|||
UiRoot? uiRoot,
|
||||
ICommandBus commands,
|
||||
RetailWindowOpacityController? chatOpacity = null,
|
||||
Action<string>? log = null)
|
||||
Action<string>? log = null,
|
||||
// Campaign OP slice OP6: the live engine reference — see
|
||||
// ApplyAudio's doc. Optional/trailing so every pre-existing
|
||||
// construction site keeps compiling unchanged (matches
|
||||
// chatOpacity/log's own optional-trailing shape).
|
||||
OpenAlAudioEngine? audio = null)
|
||||
: this(
|
||||
displayWindow,
|
||||
new RuntimeQualityApplicationTarget(
|
||||
|
|
@ -265,7 +279,8 @@ internal sealed class RuntimeSettingsTargets : IRuntimeSettingsTargets
|
|||
log,
|
||||
chatOpacity is null
|
||||
? NullRuntimeChatOpacityTarget.Instance
|
||||
: new RuntimeChatOpacityTarget(chatOpacity))
|
||||
: new RuntimeChatOpacityTarget(chatOpacity),
|
||||
audio)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -275,7 +290,8 @@ internal sealed class RuntimeSettingsTargets : IRuntimeSettingsTargets
|
|||
IRuntimeUiLockTarget uiLock,
|
||||
ICommandBus commands,
|
||||
Action<string>? log = null,
|
||||
IRuntimeChatOpacityTarget? chatOpacity = null)
|
||||
IRuntimeChatOpacityTarget? chatOpacity = null,
|
||||
OpenAlAudioEngine? audio = null)
|
||||
{
|
||||
_displayWindow = displayWindow
|
||||
?? throw new ArgumentNullException(nameof(displayWindow));
|
||||
|
|
@ -284,11 +300,22 @@ internal sealed class RuntimeSettingsTargets : IRuntimeSettingsTargets
|
|||
_chatOpacity = chatOpacity ?? NullRuntimeChatOpacityTarget.Instance;
|
||||
_commands = commands ?? throw new ArgumentNullException(nameof(commands));
|
||||
_log = log ?? Console.WriteLine;
|
||||
_audio = audio;
|
||||
}
|
||||
|
||||
public void ApplyDisplayWindowState(DisplaySettings display) =>
|
||||
_displayWindow.Apply(display);
|
||||
|
||||
/// <summary>Campaign OP slice OP6: reuses the SAME static helper the
|
||||
/// startup path (<see cref="RuntimeSettingsStartupTargets.ApplyAudio"/>)
|
||||
/// already runs — one mixer-apply implementation, two call sites (once
|
||||
/// at process start, now also on every <c>RuntimeSettingsController.
|
||||
/// SaveAudio</c>). <see langword="null"/> engine (a headless/no-audio
|
||||
/// host) is a silent no-op, matching every other optional target in
|
||||
/// this class.</summary>
|
||||
public void ApplyAudio(AudioSettings audio) =>
|
||||
RuntimeSettingsStartupTargets.ApplyAudio(_audio, audio);
|
||||
|
||||
public void ApplyQuality(QualitySettings quality)
|
||||
{
|
||||
_quality.SetAlphaToCoverage(quality.AlphaToCoverage);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue