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
|
|
@ -18,16 +18,56 @@ namespace AcDream.UI.Abstractions.Panels.Settings;
|
|||
/// who never opens the Audio tab gets identical behaviour to the
|
||||
/// previous env-var-only world.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Campaign OP slice OP6 (2026-08-11): the six trailing fields are the
|
||||
/// remaining Config-tab "Sound Options" rows, byte-verified from
|
||||
/// <c>gmClient::InitUIPreferences @0x004035b0</c>
|
||||
/// (<c>UIPreferences::AttachPreference</c> calls). <see cref="SfxDisabled"/>/
|
||||
/// <see cref="AmbientDisabled"/> gate the EXISTING live <see cref="Sfx"/>/
|
||||
/// <see cref="Ambient"/> knobs (<c>RuntimeSettingsStartupTargets.ApplyAudio</c>
|
||||
/// sends 0 to the engine when disabled, else the slider value — retail's
|
||||
/// own <c>Sound_SoundDisabled</c>/<c>Sound_AmbientSoundDisabled</c> toggle
|
||||
/// halves of the SAME <c>UIOption_CheckboxSlider</c> row). The other four
|
||||
/// fields are honest store-only round-trips (register row, OP6): retail
|
||||
/// registers <c>Sound_InterfaceSoundVolume</c>/<c>Sound_InterfaceSoundDisabled</c>
|
||||
/// and then never reads them either — AP-174 already documents "interface
|
||||
/// sounds are scaled by the EFFECT knob" — so acdream matches retail's own
|
||||
/// dead-knob behaviour rather than building a working one; <see cref="SoundFeatures"/>
|
||||
/// (Stereo/Mono) and <see cref="PlaySoundOnlyWhenActive"/> have no acdream
|
||||
/// mixer-channel-count or window-focus-mute consumer.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed record AudioSettings(
|
||||
float Master,
|
||||
float Sfx,
|
||||
float Ambient)
|
||||
float Ambient,
|
||||
// OP6: Sound Options menu row — Sound_SoundFeatures (Stereo=0/Mono=1).
|
||||
// Store-only: acdream's OpenAL backend has no channel-count toggle.
|
||||
int SoundFeatures = 0,
|
||||
// OP6: the toggle halves of the Sound/Ambient volume trios
|
||||
// (Sound_SoundDisabled / Sound_AmbientSoundDisabled). LIVE — gate the
|
||||
// existing Sfx/Ambient knobs (RuntimeSettingsStartupTargets.ApplyAudio).
|
||||
bool SfxDisabled = true,
|
||||
bool AmbientDisabled = true,
|
||||
// OP6: the Interface Sound trio — retail's own dead knob (AP-174).
|
||||
// Store-only.
|
||||
bool InterfaceDisabled = true,
|
||||
float InterfaceVolume = 1.0f,
|
||||
// OP6: Sound_PlaySoundOnlyWhenActive — store-only, no window-focus
|
||||
// mute subsystem exists.
|
||||
bool PlaySoundOnlyWhenActive = true)
|
||||
{
|
||||
/// <summary>
|
||||
/// Values used on first launch. Retail's own defaults are 1.0 for every
|
||||
/// sound preference (<c>SoundManager::InitPrefs</c> @ <c>0x005503F0</c>),
|
||||
/// so ambient starts at unity rather than the invented 0.8.
|
||||
/// so ambient starts at unity rather than the invented 0.8. The six OP6
|
||||
/// trailing fields default to retail's OWN byte-verified
|
||||
/// <c>gmClient::InitUIPreferences</c> literals: <c>SetDefaultValue(1,
|
||||
/// 0x3f800000)</c> on every trio (toggle=checked/"Disabled"=true,
|
||||
/// slider=1.0) — retail's 2013 EoR build genuinely ships every Sound
|
||||
/// category checkbox CHECKED (disabled) by default; ported faithfully,
|
||||
/// not "fixed".
|
||||
/// </summary>
|
||||
public static AudioSettings Default { get; } = new(
|
||||
Master: 1.0f,
|
||||
|
|
|
|||
|
|
@ -21,14 +21,26 @@ public sealed record CameraTurningSettings(
|
|||
float AdjustmentSpeed,
|
||||
float MouseLookSensitivity,
|
||||
bool AlignToSlope,
|
||||
bool InvertMouseLookYAxis)
|
||||
bool InvertMouseLookYAxis,
|
||||
// Campaign OP slice OP6: Input_UseMouseTurning — the Config tab's OWN
|
||||
// client-local UIPreference checkbox (gmClient::InitUIPreferences,
|
||||
// AttachPreference(&Input_UseMouseTurning, 4, ...)), DISTINCT from the
|
||||
// server-synced PlayerOption.UseMouseTurning bit the Gameplay tab's
|
||||
// "Use Mouse Turning Settings" macro sends via SetSingleCharacterOption
|
||||
// (0x0005) — that bit is NOT modeled here (MouseTurningSettingsMacro's
|
||||
// own sixth target). Store-only, same TS-74 disposition as the five
|
||||
// fields above: acdream has no persistent mouse-turning camera mode for
|
||||
// EITHER preference to drive.
|
||||
bool UseMouseTurning = false)
|
||||
{
|
||||
/// <summary>
|
||||
/// Retail's ORDINARY Config-tab defaults (NOT the mouse-turning macro's
|
||||
/// targets — <c>gmConfigUI::InitOptions @0x0049E400</c>, research doc §4):
|
||||
/// Stiffness 0.45 (<c>0x3EE66666</c>), AdjustmentSpeed 40.0
|
||||
/// (<c>0x42200000</c>), MouseLookSensitivity 0.55 (<c>0x3F0CCCCD</c>),
|
||||
/// AlignToSlope on, InvertMouseLookYAxis off.
|
||||
/// AlignToSlope on, InvertMouseLookYAxis off, UseMouseTurning off
|
||||
/// (<c>gmConfigUI::InitOptions @0x0049E7AA</c>:
|
||||
/// <c>AddToggleOption(this, &Input_UseMouseTurning)->SetDefaultValue(0)</c>).
|
||||
/// </summary>
|
||||
public static CameraTurningSettings Default { get; } = new(
|
||||
Stiffness: 0.45f,
|
||||
|
|
|
|||
|
|
@ -91,7 +91,19 @@ public sealed record ChatSettings(
|
|||
// retail 0.5-while-idle fade by default, but the Settings → Chat
|
||||
// transparency slider remains fully user-settable (AP-190).
|
||||
float DefaultOpacity = 1.0f,
|
||||
float ActiveOpacity = 1.0f)
|
||||
float ActiveOpacity = 1.0f,
|
||||
// Campaign OP slice OP6: the Config tab's "UI Options" section —
|
||||
// UI_ChatFontFace (retail Windows TrueType face name, enum choices
|
||||
// starting "Arial") and UI_ChatFontSize (index into
|
||||
// Tiny/Small/Medium/Large/XLarge, gmClient::InitUIPreferences
|
||||
// @0x0040387b/@0x00403a1a). Deliberately NOT the same field as
|
||||
// FontSize above: FontSize is acdream's own live 10..20pt render
|
||||
// knob with no verified index-to-point mapping to retail's five-tier
|
||||
// enum, and acdream's text rendering has no arbitrary system-font-face
|
||||
// swap (DAT-baked/bitmap fonts only) — both new fields are honest
|
||||
// store-only round-trips (register row, OP6).
|
||||
int ChatFontFace = 2,
|
||||
int ChatFontSizeIndex = 1)
|
||||
{
|
||||
/// <summary>
|
||||
/// N4 (CH3 Opus review): matches ACE's ACTUAL
|
||||
|
|
|
|||
|
|
@ -16,9 +16,19 @@ public enum ParticleRange
|
|||
|
||||
/// <summary>
|
||||
/// Display-related preferences persisted to <c>settings.json</c>.
|
||||
/// Modern addition (no retail equivalent for FOV / vsync etc) — replaces
|
||||
/// the various <c>ACDREAM_*</c> environment variables for resolution +
|
||||
/// windowed mode with an in-game UI.
|
||||
/// Originally documented as "no retail equivalent for FOV / vsync etc" —
|
||||
/// Campaign OP slice OP6's Config-tab research corrected that: retail's
|
||||
/// <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/
|
||||
/// Fullscreen are LIVE on save (<c>RuntimeSettingsTargets.
|
||||
/// ApplyDisplayWindowState</c> resizes the window immediately); VSync/FOV/
|
||||
/// Gamma apply at the next launch only (<c>RuntimeSettingsController.
|
||||
/// ApplyStartup</c>), matching this record's pre-existing behaviour — OP6
|
||||
/// did not change when these three take effect, only how they're reached.
|
||||
///
|
||||
/// <para>
|
||||
/// Records are immutable; mutation goes through
|
||||
|
|
@ -34,7 +44,26 @@ public sealed record DisplaySettings(
|
|||
float Gamma,
|
||||
bool ShowFps,
|
||||
QualityPreset Quality,
|
||||
ParticleRange ParticleRange)
|
||||
ParticleRange ParticleRange,
|
||||
// Campaign OP slice OP6: the Config tab's "Graphics Options" +
|
||||
// "Rendering Quality Options" rows with no acdream renderer consumer —
|
||||
// the world renderer is Vulkan + one aggregate QualityPreset, not
|
||||
// per-feature knobs (register row, OP6). Persisted faithfully; every
|
||||
// default below is retail's own byte-verified
|
||||
// gmClient::InitUIPreferences / gmConfigUI::InitOptions literal.
|
||||
bool AutomaticDegrades = false,
|
||||
float GraphicsPerformance = 0f,
|
||||
float DegradeDistance = 50f,
|
||||
int LandscapeTextureDetail = 2,
|
||||
int EnvironmentTextureDetail = 1,
|
||||
int TextureFiltering = 1,
|
||||
// UNRESOLVED (OP6, cite in register row): retail's own
|
||||
// SetDefaultValue(8) does not index its 6-entry SetEnumChoices array
|
||||
// (VeryLow..Extreme) — reproduced faithfully as an opaque int, not
|
||||
// guessed into a clamped index.
|
||||
int LandscapeDrawDistance = 8,
|
||||
bool BuildingDetailTextures = true,
|
||||
bool MultiPassAlpha = false)
|
||||
{
|
||||
/// <summary>Values used on first launch / when settings.json is absent.
|
||||
/// Geometry defaults preserve the pre-L.0 runtime state: Resolution
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue