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

@ -70,7 +70,16 @@ public sealed class SettingsStore
ShowFps: ReadBool (disp, "showFps", d.ShowFps),
Quality: ReadQuality (disp, "quality", d.Quality),
ParticleRange: ReadParticleRange(
disp, "particleRange", d.ParticleRange));
disp, "particleRange", d.ParticleRange),
AutomaticDegrades: ReadBool (disp, "automaticDegrades", d.AutomaticDegrades),
GraphicsPerformance: ReadFloat(disp, "graphicsPerformance", d.GraphicsPerformance),
DegradeDistance: ReadFloat(disp, "degradeDistance", d.DegradeDistance),
LandscapeTextureDetail: ReadInt (disp, "landscapeTextureDetail", d.LandscapeTextureDetail),
EnvironmentTextureDetail:ReadInt (disp, "environmentTextureDetail",d.EnvironmentTextureDetail),
TextureFiltering: ReadInt (disp, "textureFiltering", d.TextureFiltering),
LandscapeDrawDistance: ReadInt (disp, "landscapeDrawDistance", d.LandscapeDrawDistance),
BuildingDetailTextures: ReadBool (disp, "buildingDetailTextures", d.BuildingDetailTextures),
MultiPassAlpha: ReadBool (disp, "multiPassAlpha", d.MultiPassAlpha));
}
catch (Exception ex)
{
@ -109,7 +118,13 @@ public sealed class SettingsStore
return new AudioSettings(
Master: ReadFloat(audio, "master", d.Master),
Sfx: ReadFloat(audio, "sfx", d.Sfx),
Ambient: ReadFloat(audio, "ambient", d.Ambient));
Ambient: ReadFloat(audio, "ambient", d.Ambient),
SoundFeatures: ReadInt (audio, "soundFeatures", d.SoundFeatures),
SfxDisabled: ReadBool (audio, "sfxDisabled", d.SfxDisabled),
AmbientDisabled: ReadBool (audio, "ambientDisabled", d.AmbientDisabled),
InterfaceDisabled: ReadBool (audio, "interfaceDisabled", d.InterfaceDisabled),
InterfaceVolume: ReadFloat(audio, "interfaceVolume", d.InterfaceVolume),
PlaySoundOnlyWhenActive: ReadBool (audio, "playSoundOnlyWhenActive", d.PlaySoundOnlyWhenActive));
}
catch (Exception ex)
{
@ -199,7 +214,9 @@ public sealed class SettingsStore
ChatWindow4Filter: ReadULong(chat, "chatWindow4Filter", d.ChatWindow4Filter),
ChatWindowMainFilter: ReadULong(chat, "chatWindowMainFilter", d.ChatWindowMainFilter),
DefaultOpacity: ReadFloat(chat, "defaultOpacity", d.DefaultOpacity),
ActiveOpacity: ReadFloat(chat, "activeOpacity", d.ActiveOpacity));
ActiveOpacity: ReadFloat(chat, "activeOpacity", d.ActiveOpacity),
ChatFontFace: ReadInt(chat, "chatFontFace", d.ChatFontFace),
ChatFontSizeIndex: ReadInt(chat, "chatFontSizeIndex", d.ChatFontSizeIndex));
}
catch (Exception ex)
{
@ -235,7 +252,8 @@ public sealed class SettingsStore
AdjustmentSpeed: ReadFloat(ct, "adjustmentSpeed", d.AdjustmentSpeed),
MouseLookSensitivity: ReadFloat(ct, "mouseLookSensitivity", d.MouseLookSensitivity),
AlignToSlope: ReadBool (ct, "alignToSlope", d.AlignToSlope),
InvertMouseLookYAxis: ReadBool (ct, "invertMouseLookYAxis", d.InvertMouseLookYAxis));
InvertMouseLookYAxis: ReadBool (ct, "invertMouseLookYAxis", d.InvertMouseLookYAxis),
UseMouseTurning: ReadBool (ct, "useMouseTurning", d.UseMouseTurning));
}
catch (Exception ex)
{
@ -580,6 +598,8 @@ public sealed class SettingsStore
{
["activeOpacity"] = c.ActiveOpacity,
["appearOffline"] = c.AppearOffline,
["chatFontFace"] = c.ChatFontFace,
["chatFontSizeIndex"] = c.ChatFontSizeIndex,
["chatWindow1Filter"] = c.ChatWindow1Filter,
["chatWindow2Filter"] = c.ChatWindow2Filter,
["chatWindow3Filter"] = c.ChatWindow3Filter,
@ -617,13 +637,22 @@ public sealed class SettingsStore
private static SortedDictionary<string, object> BuildDisplayObject(DisplaySettings d)
=> new(StringComparer.Ordinal)
{
["automaticDegrades"] = d.AutomaticDegrades,
["buildingDetailTextures"] = d.BuildingDetailTextures,
["degradeDistance"] = d.DegradeDistance,
["environmentTextureDetail"] = d.EnvironmentTextureDetail,
["fieldOfView"] = d.FieldOfView,
["fullscreen"] = d.Fullscreen,
["gamma"] = d.Gamma,
["graphicsPerformance"] = d.GraphicsPerformance,
["landscapeDrawDistance"] = d.LandscapeDrawDistance,
["landscapeTextureDetail"] = d.LandscapeTextureDetail,
["multiPassAlpha"] = d.MultiPassAlpha,
["particleRange"] = d.ParticleRange.ToString(),
["quality"] = d.Quality.ToString(),
["resolution"] = d.Resolution,
["showFps"] = d.ShowFps,
["textureFiltering"] = d.TextureFiltering,
["vsync"] = d.VSync,
};
@ -635,14 +664,21 @@ public sealed class SettingsStore
["invertMouseLookYAxis"] = c.InvertMouseLookYAxis,
["mouseLookSensitivity"] = c.MouseLookSensitivity,
["stiffness"] = c.Stiffness,
["useMouseTurning"] = c.UseMouseTurning,
};
private static SortedDictionary<string, object> BuildAudioObject(AudioSettings a)
=> new(StringComparer.Ordinal)
{
["ambient"] = a.Ambient,
["master"] = a.Master,
["sfx"] = a.Sfx,
["ambient"] = a.Ambient,
["ambientDisabled"] = a.AmbientDisabled,
["interfaceDisabled"] = a.InterfaceDisabled,
["interfaceVolume"] = a.InterfaceVolume,
["master"] = a.Master,
["playSoundOnlyWhenActive"] = a.PlaySoundOnlyWhenActive,
["sfx"] = a.Sfx,
["sfxDisabled"] = a.SfxDisabled,
["soundFeatures"] = a.SoundFeatures,
};
/// <summary>
@ -710,6 +746,10 @@ public sealed class SettingsStore
=> obj.TryGetProperty(name, out var el) && el.ValueKind == JsonValueKind.Number
? el.GetSingle() : fallback;
private static int ReadInt(JsonElement obj, string name, int fallback)
=> obj.TryGetProperty(name, out var el) && el.ValueKind == JsonValueKind.Number
? el.GetInt32() : fallback;
private static ulong ReadULong(JsonElement obj, string name, ulong fallback)
{
if (!obj.TryGetProperty(name, out var el) || el.ValueKind != JsonValueKind.Number)