diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 11b065b3..4f430413 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -341,7 +341,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | TS-6 | Weather particle emission suppressed — all weathery DayGroups map to Overcast (correct fog/cloud tone, no precipitation); retail's camera-attached weather subsystem not yet located in the decomp | `src/AcDream.Core/World/WeatherState.cs:200` | Decomp research verified the sky loop never reads `DefaultPesObjectId`; an earlier name-based rain spawn regressed (rained where retail didn't, 2026-04-23) — inventing a name→rain path is forbidden until the real subsystem is found | Rainy/snowy/stormy days never show retail's precipitation effects (permanent missing visuals until the subsystem is found and ported) | FUN_00508010 / FUN_0051bed0→FUN_0051bfb0 (negative findings) | | TS-7 | SkyObject `weather_enabled` gate not honored — weather-flagged sky objects (bit 0x04) always instantiate | `src/AcDream.Core/World/SkyDescLoader.cs:50` | No weather_enabled toggle exists yet; IsWeather flag parsed + documented as the gate to wire | Weather-only sky meshes (rain cylinders) appear where retail-with-weather-off suppresses them | `GameSky::MakeObject` 0x00506ee0, guard at decomp:268630 | | ~~TS-8~~ | **RETIRED 2026-07-31 (#268 stat-chain closeout).** `EnchantmentWireReader` parses the complete 0x02C2 payload and `GameEventWiring` publishes its StatMod type/key/value and bucket through the same `ActiveEnchantmentRecord` used at login. An end-to-end dispatch test proves a mid-session skill modifier changes `LocalPlayerState.GetEffectiveSkill` immediately. | `src/AcDream.Core.Net/Messages/EnchantmentWireReader.cs`; `src/AcDream.Core.Net/GameEventWiring.cs`; `tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs` | — | — | `CEnchantmentRegistry::EnchantAttribute @ 0x00594570`; `CEnchantmentRegistry::EnchantSkill @ 0x005947B0`; holtburger `messages/magic/types.rs` | -| TS-9 | MP3 (0x55) and MS-ADPCM (0x02) waves undecoded — affected sounds skipped; retail decoded both via winmm ACM | `src/AcDream.Core/Audio/WaveDecoder.cs:33` | Managed decoder (NAudio or similar) deferred; PCM covers the vast majority of ~3500 waves | Any MP3 (common for music-ish clips) or ADPCM cue plays as silence where retail plays it | winmm ACM path (r05 §2.1) | +| TS-9 | **RE-SCOPED 2026-08-08 (Campaign A slice A6) — the blast radius is one wave, measured.** MP3 (0x55) and MS-ADPCM (0x02) waves still decode to null and play as silence where retail decoded both through the winmm ACM. What changed is the size of the problem: an independent walk of the shipped dats found exactly **1 MP3 among 786 waves** (`0x0A000393`, a ~2 s mono clip) and the row's original "any MP3 cue, common for music-ish clips" framing was wrong — it was written when we believed retail had a music system, and retail has none. A managed decoder for one two-second asset is not worth the dependency; the honest options are a ~50-line decode or an accepted-loss row, and this row is now the accepted-loss record. The ADPCM count has NOT been measured and is the one open question here. | `src/AcDream.Core/Audio/WaveDecoder.cs:33` | Measured rather than assumed. PCM covers 785 of 786 waves. | One ~2 s clip is silent, plus an unmeasured number of ADPCM clips. | winmm ACM path; dat census in `docs/research/2026-08-08-audio-retail-dat-layer.md` §4c | | TS-14 | Setup `Flatten` ignores ParentIndex part hierarchy (treats every placement as root-local); still in production use (GameWindow hydration, SkyRenderer) | `src/AcDream.Core/Meshing/SetupMesh.cs:15` | Most Setups are flat single-level rigs where root-local equals composed; hierarchical composition deferred ("Phase 3") | Any Setup with genuinely nested parts renders them at wrong offsets — mis-assembled multi-part objects in the Flatten paths | retail Setup ParentIndex chain composition | | TS-15 | No distance-driven degrade (LOD): always close-detail slot 0; plus the **#47** static `Degrades[0]` swap for 34-part humanoids only (structural sentinel detector) | `src/AcDream.Core/Meshing/GfxObjDegradeResolver.cs:57` (+ `src/AcDream.App/Rendering/GameWindow.cs:2608`) | LOD plumbing doesn't exist; slot 0 is correct for player + nearby NPCs; #47 closed the visible low-detail-arms bug without porting UpdateViewerDistance | Distant objects render max-detail (perf + wrong visuals where far meshes intentionally differ/hide parts); a future 34-part non-humanoid matching the sentinel gets the wrong mesh swap | `CPhysicsPart::UpdateViewerDistance` 0x0050E030; ::Draw 0x0050D7A0; ::LoadGfxObjArray 0x0050DCF0 | | TS-17 | AttackConditions suffix always empty in combat chat — formatting ported, wire bitflag not plumbed (Phase I.7 follow-up) | `src/AcDream.Core/Chat/CombatChatTranslator.cs:233` | Only the wire plumbing is missing; the holtburger-ported formatter is ready | Combat log omits "[Sneak Attack]"-style suffixes retail displays — hidden combat-mechanic feedback | holtburger chat.rs:588-595 | diff --git a/docs/plans/2026-08-08-audio-parity-campaign.md b/docs/plans/2026-08-08-audio-parity-campaign.md index c50c8ca1..de89a759 100644 --- a/docs/plans/2026-08-08-audio-parity-campaign.md +++ b/docs/plans/2026-08-08-audio-parity-campaign.md @@ -1,6 +1,6 @@ # Campaign A — Audio retail-feel parity -**Status: PROPOSED 2026-08-08 — awaiting user go.** Research phase complete; +**Status: CODE-COMPLETE 2026-08-08 — awaiting the user listening gate.** All six slices landed (see the ledger). Research phase complete; six-lane named-retail decode done, all load-bearing claims byte-verified against the PDB-paired 2013 binary (BN pseudo-C alone was NOT sufficient — see "BN traps" below). @@ -303,8 +303,54 @@ global kill switch. | Slice | Status | Commit | Gates | |---|---|---|---| | A1 | **COMPLETE** 2026-08-08 | `c69b3bde` | 42 Core audio tests; full Release suite 11,563 passed / 4 skipped / 0 failed. Closes #355. | -| A2 | **COMPLETE** 2026-08-08 | `6d0156cb` | 118 Core audio tests (mixer + voice pool + cookbook); full Release suite 11,639 passed / 4 skipped / 0 failed. Opus review run and applied — 2 HIGH (pan-law saturation, stale `FUN_00550ad0` header), 5 MEDIUM (untested clamp order / pan truncation / voice pool, dead `PlayingGain`, duplicated heading helper), 5 LOW. Retires AP-28; files AP-173, AP-174, TS-64, TS-65. **Owed: user listening gate.** | -| A3 | **COMPLETE** 2026-08-08 | `3fae0c7d` | 14 wire-conformance tests + 5 controller tests; full Release suite 11,658 passed / 4 skipped / 0 failed. **Owed: connected gate** (melee hit / pickup / lifestone audible against ACE). | -| A4 | **COMPLETE** 2026-08-08 | `489ea6ad` | UI bank DID resolved from the dats (`0x2000004B`, content-verified: exactly the 32 `UI_*` slots) + 21-case environ table, 30 new Core tests; full Release suite 11,691 passed / 4 skipped. Retires TS-54; narrows AP-115 to notice-only. **Owed: connected gate** (`@environs` thunder + recall cues audible). **Suite note:** two load-dependent measurement flakes were observed on separate full-suite runs (`RuntimeCollisionReportingStateTests.WarmedSteadyContactRefreshDoesNotAllocate`, and one unnamed Core.Net test); both pass in isolation and neither touches audio. | -| A5 | **COMPLETE** 2026-08-08 | `e81bc474` | 46 ambient conformance tests; full Release suite 11,739 passed / 4 skipped. Opus review run and applied — it caught a FATAL frame bug (cell offsets built in absolute world coordinates while the listener is in the streamed frame: every contribution culled at ~32 km, feature silent with no error), a per-entry vs per-cell denominator error that would have pushed multi-entry beds under the audibility floor, an infinite loop on a zero play-rate, and newly-audible ambients not firing until a full period later. Also moved beds onto retail's single 16-voice priority pool and made the in-block direction test XY-only. Retires TS-29; files TS-66 (`seen_outside` interiors), TS-67 (in-plane weight). **Owed: user listening gate.** | -| A6 | — | — | — | +| A2 | **COMPLETE** 2026-08-08 | `e42b9948` | 118 Core audio tests (mixer + voice pool + cookbook); full Release suite 11,639 passed / 4 skipped / 0 failed. Opus review run and applied — 2 HIGH (pan-law saturation, stale `FUN_00550ad0` header), 5 MEDIUM (untested clamp order / pan truncation / voice pool, dead `PlayingGain`, duplicated heading helper), 5 LOW. Retires AP-28; files AP-173, AP-174, TS-64, TS-65. **Owed: user listening gate.** | +| A3 | **COMPLETE** 2026-08-08 | `8bc458fb` | 14 wire-conformance tests + 5 controller tests; full Release suite 11,658 passed / 4 skipped / 0 failed. **Owed: connected gate** (melee hit / pickup / lifestone audible against ACE). | +| A4 | **COMPLETE** 2026-08-08 | `6eaa490b` | UI bank DID resolved from the dats (`0x2000004B`, content-verified: exactly the 32 `UI_*` slots) + 21-case environ table, 30 new Core tests; full Release suite 11,691 passed / 4 skipped. Retires TS-54; narrows AP-115 to notice-only. **Owed: connected gate** (`@environs` thunder + recall cues audible). **Suite note:** two load-dependent measurement flakes were observed on separate full-suite runs (`RuntimeCollisionReportingStateTests.WarmedSteadyContactRefreshDoesNotAllocate`, and one unnamed Core.Net test); both pass in isolation and neither touches audio. | +| A5 | **COMPLETE** 2026-08-08 | `7c4dd1ad` | 46 ambient conformance tests; full Release suite 11,739 passed / 4 skipped. Opus review run and applied — it caught a FATAL frame bug (cell offsets built in absolute world coordinates while the listener is in the streamed frame: every contribution culled at ~32 km, feature silent with no error), a per-entry vs per-cell denominator error that would have pushed multi-entry beds under the audibility floor, an infinite loop on a zero play-rate, and newly-audible ambients not firing until a full period later. Also moved beds onto retail's single 16-voice priority pool and made the in-block direction test XY-only. Retires TS-29; files TS-66 (`seen_outside` interiors), TS-67 (in-plane weight). **Owed: user listening gate.** | +| A6 | **COMPLETE** 2026-08-08 | `1b083463` | Full Release suite 11,739 passed / 4 skipped. Deleted the music API (`PlayMusic`/`StopMusic`/`MusicVolume` + the `AudioSettings.Music` knob); exposed the Ambient slider now that A5 drives it; reset the invented 0.8 ambient default to retail's 1.0; SUPERSEDED banner on `r05-audio-sound.md` listing its five wrong sections; TS-9 re-scoped to the measured one-wave blast radius. **Deferred:** #321's decode-dedup race (a pre-existing concurrency flake, not audio-parity behaviour — not fixed speculatively without reproducing it). | + +--- + +## CAMPAIGN CLOSEOUT (code-complete 2026-08-08) + +Six slices, six commits, `c69b3bde` → A6. The full Release suite ends at +**11,739 passed / 4 skipped / 0 failed**, up from 11,563 at campaign start; +the audio subsystem went from 42 tests (one of which pinned the wrong model) +to ~215 conformance tests written against byte-decoded values. + +**What was wrong, and is now right:** + +| Was | Now | +|---|---| +| Probability treated as a selection weight, and skipped entirely for the 4,183/4,184 single-entry sounds | Retail's uniform `(n−1)` pick plus an independent Bernoulli silence gate | +| OpenAL 3-D spatialization, `2/d` falloff, no cutoff | Retail's CPU 2-D model: `25·vol/d²` past a 5 m knee, −50 dB no-allocate floor (~94 m), ±15 dB sine pan with a 5 m dead zone | +| Voices evicted by gain | Evicted by DAT-authored float priority, strictly-less, ring order | +| `0xF750` unparsed — every server cue silent | Parsed, guid-queued-and-replayed, played at the wire volume | +| No interface sound bank | Bank DID resolved from the dats' EnumIDMap chain; portal cues + 21 AdminEnvirons stingers live | +| No ambient system at all | Region-authored, per-land-cell, 3×3 ring, deadline-queue one-shots with terrain crossfade | +| A music API | Deleted — retail has no music system | + +**Process notes worth carrying forward:** + +1. **Binary Ninja could not be trusted anywhere in this subsystem.** Five + float compares render inverted or with elided constants; `GetAttenuation` + prints `* 0f`, which ports as silence at every distance. Every load-bearing + value here came from byte-decoding the PDB-paired binary. The lane notes + record the verified values; a future reader should decode rather than + re-read the pseudo-C. +2. **Two research notes were wrong and were corrected in place** — lane 1's + 30 m decibel row (contradicted its own gain column) and lane 5's + transposed `GetByEnum` arguments (which would have made the UI bank + unresolvable). Both were caught by recomputing rather than copying. +3. **The reviews earned their cost.** A2's review caught a pan mapping that + saturated to full separation where retail gives 15 dB. A5's caught a + coordinate-frame error that would have made the entire ambient system + silent with nothing logged — the tests passed, the build was green, and it + would have failed only at the listening gate. +4. **An architecture guard caught a design error the tests could not**: + `ExtractedUpdateOwners_DoNotRetainAnonymousCallbacks` rejected an + `Action` frame hook and forced the typed `IAmbientFramePhase`. + +**Still owed:** the user listening gate (A2 falloff, A4 cues, A5 ambients) and +the connected gates for A3/A4. Open rows: AP-173, AP-174, TS-64, TS-65, TS-66, +TS-67, TS-9 (re-scoped), #321. diff --git a/docs/research/deepdives/r05-audio-sound.md b/docs/research/deepdives/r05-audio-sound.md index 069ad413..7dcc0fea 100644 --- a/docs/research/deepdives/r05-audio-sound.md +++ b/docs/research/deepdives/r05-audio-sound.md @@ -1,5 +1,35 @@ # R5 — Audio System Deep Dive +> **SUPERSEDED 2026-08-08 (Campaign A).** This document predates the named +> retail decomp and the byte-level verification pass; several of its +> load-bearing sections are wrong and were used to build code that had to be +> undone. Read the six lane notes instead — +> `docs/research/2026-08-08-audio-retail-*.md` — and +> `docs/plans/2026-08-08-audio-parity-campaign.md` for the summary. +> +> Specifically corrected: +> +> - **§5.1 falloff.** The `FUN_00550c30` reading is not retail's curve. +> `SoundManager::GetAttenuation` @ `0x00550020` is +> `g = dist < 5 ? vol : 25·vol/dist²`, clamped BEFORE one master multiply, +> then `ceil(20·log10 g)` with a hard −50 dB no-allocate floor (audible +> radius ≈94.2 m). Binary Ninja elides the x87 constants and prints `* 0f`; +> porting its rendering yields silence at every distance. +> - **§5.2 pan / §5.3 pool.** Pan is `(int)(−15·sin Δbearing)`, truncating +> toward zero, dead centre inside 5 m, with no front/back and no elevation. +> The voice allocator is `PlaySoundInternal` @ `0x0054FEC0` and it evicts on +> DAT-authored float PRIORITY, never gain; `FUN_00550AD0` is a hash-table +> constructor. +> - **§4 selection.** Probability is a Bernoulli play/skip GATE at the play +> site, not a selection weight; selection is a uniform index over `(n−1)`. +> - **§6 music.** There is no music system in retail EoR. The linked MIDI +> player has zero callers. The `PlayMusic` API this section inspired modelled +> dead code and was deleted at slice A6. +> - **§7 ambient.** Ambients are region-authored, per-LAND-CELL over a 3×3 +> landblock ring, and play as re-fired one-shots off an absolute-deadline +> queue — not landblock-attached loops. Indoors is silent by design. + + Ground-truth port plan for acdream's audio subsystem, derived from decompiled `acclient.exe`, cross-referenced against ACE (server), ACViewer (dat loader), holtburger (client protocol), AC2D (C++ client), and DatReaderWriter (dat diff --git a/src/AcDream.App/Audio/OpenAlAudioEngine.cs b/src/AcDream.App/Audio/OpenAlAudioEngine.cs index 2cd75c7f..4709e625 100644 --- a/src/AcDream.App/Audio/OpenAlAudioEngine.cs +++ b/src/AcDream.App/Audio/OpenAlAudioEngine.cs @@ -138,7 +138,6 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescen // ── Public volume knobs ────────────────────────────────────────────────── public float MasterVolume { get; set; } = 1f; public float SfxVolume { get; set; } = 1f; - public float MusicVolume { get; set; } = 0.7f; public float AmbientVolume{ get; set; } = 0.8f; public bool IsAvailable => _available; @@ -555,8 +554,6 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescen /// private float AmbientMaster => MasterVolume * AmbientVolume; - public void PlayMusic(string resourceName, bool loop) { /* R5 §6 MIDI — not ported */ } - public void StopMusic() { /* ditto */ } // ── Private helpers ────────────────────────────────────────────────────── diff --git a/src/AcDream.App/Settings/RuntimeSettingsTargets.cs b/src/AcDream.App/Settings/RuntimeSettingsTargets.cs index 2be9d202..e772e08f 100644 --- a/src/AcDream.App/Settings/RuntimeSettingsTargets.cs +++ b/src/AcDream.App/Settings/RuntimeSettingsTargets.cs @@ -125,7 +125,6 @@ internal sealed class RuntimeSettingsStartupTargets : IRuntimeSettingsStartupTar if (engine is not { IsAvailable: true }) return; engine.MasterVolume = audio.Master; - engine.MusicVolume = audio.Music; engine.SfxVolume = audio.Sfx; engine.AmbientVolume = audio.Ambient; } diff --git a/src/AcDream.Core/Audio/AudioModel.cs b/src/AcDream.Core/Audio/AudioModel.cs index ab28e7f9..2f023812 100644 --- a/src/AcDream.Core/Audio/AudioModel.cs +++ b/src/AcDream.Core/Audio/AudioModel.cs @@ -52,10 +52,17 @@ public sealed class WaveData /// public interface IAudioEngine : IDisposable { - /// Set master volume [0..1]. + /// + /// acdream's own master, on top of retail's two live knobs. Retail has + /// three preferences — effect, ambient, and an interface one it registers + /// and never reads — and NO master and no music knob. See AP-174. + /// float MasterVolume { get; set; } + + /// Retail's effect_sound_volume. float SfxVolume { get; set; } - float MusicVolume { get; set; } + + /// Retail's ambient_sound_volume (applied twice — TS-65). float AmbientVolume{ get; set; } /// @@ -86,7 +93,17 @@ public interface IAudioEngine : IDisposable // up a source that was never created. `AmbientSoundController` + // `AmbientSoundScheduler` carry the real model. - /// Start music (fades out previous if any). - void PlayMusic(string resourceName, bool loop); - void StopMusic(); + // A `PlayMusic(resourceName, loop)` / `StopMusic()` pair and a `MusicVolume` + // knob lived here until 2026-08-08 (Campaign A slice A6). They modelled a + // subsystem retail does not have. The EoR client links a complete winmm + // midiStream player and never feeds it: `midiPlay` @ 0x00553390 has zero + // callers (its only textual occurrences in the 65 MB decomp are its own + // definition and its own queue drainer), both MIDI callbacks are statically + // null, the string "music" appears zero times, `SoundType` has no music + // member, `SoundManager::InitPrefs` registers no music preference, and the + // retail install ships no .mid/.mp3/.wav at all. The string-keyed signature + // was itself the tell — every other entry point in this interface is + // DID-keyed. What players remember as music is the AdminEnvirons UI stinger + // family (see `EnvironSoundCueMap`) and the intro AVI's audio track. + // Evidence: docs/research/2026-08-08-audio-retail-music-absence.md. } diff --git a/src/AcDream.UI.Abstractions/Panels/Settings/AudioSettings.cs b/src/AcDream.UI.Abstractions/Panels/Settings/AudioSettings.cs index 43a1b474..1821efa1 100644 --- a/src/AcDream.UI.Abstractions/Panels/Settings/AudioSettings.cs +++ b/src/AcDream.UI.Abstractions/Panels/Settings/AudioSettings.cs @@ -1,10 +1,17 @@ namespace AcDream.UI.Abstractions.Panels.Settings; /// -/// Audio mixer preferences persisted to settings.json. Drives the -/// existing Phase E.2 OpenAL engine — the host wires these values into -/// OpenAlAudioEngine.MasterVolume / SfxVolume / -/// MusicVolume / AmbientVolume on Save and on startup. +/// Audio mixer preferences persisted to settings.json. The host wires +/// these into OpenAlAudioEngine.MasterVolume / SfxVolume / +/// AmbientVolume on Save and on startup. +/// +/// +/// A Music field lived here until 2026-08-08 (Campaign A slice A6). +/// Retail has no music system to turn down — see the note on +/// IAudioEngine — so the knob moved nothing. Old settings.json +/// files carrying a "music" key still load: the reader simply ignores +/// keys it does not know, and the next save drops it. +/// /// /// /// Defaults match the engine's hard-coded starting values so a user @@ -14,15 +21,16 @@ namespace AcDream.UI.Abstractions.Panels.Settings; /// public sealed record AudioSettings( float Master, - float Music, float Sfx, float Ambient) { - /// Values used on first launch. Mirror the engine's - /// constructor-default Volume properties. + /// + /// Values used on first launch. Retail's own defaults are 1.0 for every + /// sound preference (SoundManager::InitPrefs @ 0x005503F0), + /// so ambient starts at unity rather than the invented 0.8. + /// public static AudioSettings Default { get; } = new( Master: 1.0f, - Music: 0.7f, Sfx: 1.0f, - Ambient: 0.8f); + Ambient: 1.0f); } diff --git a/src/AcDream.UI.Abstractions/Panels/Settings/SettingsPanel.cs b/src/AcDream.UI.Abstractions/Panels/Settings/SettingsPanel.cs index 6008a6a2..fc928115 100644 --- a/src/AcDream.UI.Abstractions/Panels/Settings/SettingsPanel.cs +++ b/src/AcDream.UI.Abstractions/Panels/Settings/SettingsPanel.cs @@ -253,15 +253,15 @@ public sealed class SettingsPanel : IPanel } /// - /// Render the Audio tab — Master + SFX volume sliders (live preview - /// against the running OpenAL engine). Music + Ambient fields exist - /// in and persist round-trip, but their - /// sliders are intentionally hidden here because the underlying - /// engine paths (PlayMusic / StartAmbient) are stubbed for R5 MIDI - /// playback that hasn't shipped yet — exposing the sliders would be - /// "moving a knob that does nothing." When R5 lands, restore the - /// hidden sliders below and the JSON-persisted values will already - /// be in place. + /// Render the Audio tab — Master, SFX and Ambient volume sliders, all with + /// live preview against the running engine. + /// + /// + /// Ambient was hidden until Campaign A slice A5 because nothing drove it; + /// the region ambient soundscape now does, so the knob moves something and + /// is exposed. The Music slider is gone entirely: retail has no music + /// system, so there was never anything for it to turn down. + /// /// private void RenderAudioTab(IPanelRenderer renderer) { @@ -275,16 +275,9 @@ public sealed class SettingsPanel : IPanel if (renderer.SliderFloat("SFX", ref sfx, 0f, 1f)) _vm.SetAudio(a with { Sfx = sfx }); - // Music + Ambient hidden until R5 MIDI / ambient-loop engines - // exist. AudioSettings still carries the fields so the JSON - // round-trips and a future client doesn't drop them on save. - // - // float music = a.Music; - // if (renderer.SliderFloat("Music", ref music, 0f, 1f)) - // _vm.SetAudio(a with { Music = music }); - // float ambient = a.Ambient; - // if (renderer.SliderFloat("Ambient", ref ambient, 0f, 1f)) - // _vm.SetAudio(a with { Ambient = ambient }); + float ambient = a.Ambient; + if (renderer.SliderFloat("Ambient", ref ambient, 0f, 1f)) + _vm.SetAudio(a with { Ambient = ambient }); renderer.Spacing(); renderer.TextWrapped( diff --git a/src/AcDream.UI.Abstractions/Panels/Settings/SettingsStore.cs b/src/AcDream.UI.Abstractions/Panels/Settings/SettingsStore.cs index 0c238961..0656dd7a 100644 --- a/src/AcDream.UI.Abstractions/Panels/Settings/SettingsStore.cs +++ b/src/AcDream.UI.Abstractions/Panels/Settings/SettingsStore.cs @@ -108,7 +108,6 @@ public sealed class SettingsStore var d = AudioSettings.Default; return new AudioSettings( Master: ReadFloat(audio, "master", d.Master), - Music: ReadFloat(audio, "music", d.Music), Sfx: ReadFloat(audio, "sfx", d.Sfx), Ambient: ReadFloat(audio, "ambient", d.Ambient)); } @@ -588,7 +587,6 @@ public sealed class SettingsStore { ["ambient"] = a.Ambient, ["master"] = a.Master, - ["music"] = a.Music, ["sfx"] = a.Sfx, }; diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/AudioSettingsTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/AudioSettingsTests.cs index 42d6f817..28917d7a 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/AudioSettingsTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/AudioSettingsTests.cs @@ -13,13 +13,13 @@ public sealed class AudioSettingsTests [Fact] public void Default_values_match_engine_constructor_defaults() { - // OpenAlAudioEngine ctor: Master=1.0, Music=0.7, Sfx=1.0, - // Ambient=0.8 — see src/AcDream.App/Audio/OpenAlAudioEngine.cs. + // Retail's SoundManager::InitPrefs @ 0x005503F0 defaults every sound + // preference to 1.0; acdream's extra master matches. The Music knob is + // gone (retail has no music system) and Ambient's invented 0.8 with it. var d = AudioSettings.Default; Assert.Equal(1.0f, d.Master); - Assert.Equal(0.7f, d.Music); Assert.Equal(1.0f, d.Sfx); - Assert.Equal(0.8f, d.Ambient); + Assert.Equal(1.0f, d.Ambient); } [Fact] @@ -35,8 +35,8 @@ public sealed class AudioSettingsTests [Fact] public void With_expression_clones_one_field() { - var d = AudioSettings.Default with { Music = 0.25f }; - Assert.Equal(0.25f, d.Music); + var d = AudioSettings.Default with { Ambient = 0.25f }; + Assert.Equal(0.25f, d.Ambient); // Other fields untouched. Assert.Equal(AudioSettings.Default.Master, d.Master); Assert.Equal(AudioSettings.Default.Sfx, d.Sfx); diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsPanelTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsPanelTests.cs index 7b420af9..55229823 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsPanelTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsPanelTests.cs @@ -349,11 +349,11 @@ public sealed class SettingsPanelTests [Fact] public void Audio_tab_when_active_renders_implemented_volume_sliders() { - // L.0 ships Master + SFX only — Music + Ambient sliders are - // hidden until R5 MIDI / ambient-loop engines exist. The - // AudioSettings record still carries those fields so the - // JSON round-trips, but the panel doesn't surface a slider - // that wouldn't actually do anything. + // The rule is "no slider that does nothing", and Campaign A changed + // what qualifies. Ambient is now surfaced because slice A5 gave it a + // region ambient system to drive. Music is gone entirely — retail has + // no music system, so slice A6 deleted the field rather than leaving a + // knob that could never do anything. var (panel, _, _, _) = Build(); var r = new FakePanelRenderer { ActiveTabLabel = "Audio" }; @@ -363,8 +363,8 @@ public sealed class SettingsPanelTests .Select(c => (string)c.Args[0]!).ToList(); Assert.Contains("Master", sliders); Assert.Contains("SFX", sliders); + Assert.Contains("Ambient", sliders); Assert.DoesNotContain("Music", sliders); - Assert.DoesNotContain("Ambient", sliders); } [Fact] diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsStoreTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsStoreTests.cs index b6012b6c..c602927a 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsStoreTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsStoreTests.cs @@ -156,7 +156,7 @@ public sealed class SettingsStoreTests : System.IDisposable public void SaveAudio_then_LoadAudio_round_trips_all_fields() { var store = new SettingsStore(_tempPath); - var original = new AudioSettings(Master: 0.3f, Music: 0.45f, Sfx: 0.9f, Ambient: 0.6f); + var original = new AudioSettings(Master: 0.3f, Sfx: 0.9f, Ambient: 0.6f); store.SaveAudio(original); var loaded = store.LoadAudio(); @@ -178,7 +178,6 @@ public sealed class SettingsStoreTests : System.IDisposable var loaded = store.LoadAudio(); Assert.Equal(0.25f, loaded.Master); - Assert.Equal(AudioSettings.Default.Music, loaded.Music); Assert.Equal(AudioSettings.Default.Sfx, loaded.Sfx); Assert.Equal(AudioSettings.Default.Ambient, loaded.Ambient); } @@ -200,10 +199,10 @@ public sealed class SettingsStoreTests : System.IDisposable { // Reverse order — audio must survive a subsequent display save. var store = new SettingsStore(_tempPath); - store.SaveAudio(AudioSettings.Default with { Music = 0.1f }); + store.SaveAudio(AudioSettings.Default with { Ambient = 0.1f }); store.SaveDisplay(DisplaySettings.Default with { ShowFps = true }); - Assert.Equal(0.1f, store.LoadAudio().Music); + Assert.Equal(0.1f, store.LoadAudio().Ambient); Assert.True(store.LoadDisplay().ShowFps); } diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsVMTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsVMTests.cs index b4a15961..7dafceeb 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsVMTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsVMTests.cs @@ -360,7 +360,7 @@ public sealed class SettingsVMTests [Fact] public void AudioDraft_initial_value_matches_persisted() { - var custom = AudioSettings.Default with { Master = 0.3f, Music = 0.1f }; + var custom = AudioSettings.Default with { Master = 0.3f, Ambient = 0.1f }; var (vm, _, _, _, _, _, _, _, _, _) = Build(persistedAudio: custom); Assert.Equal(custom, vm.AudioDraft); Assert.False(vm.HasUnsavedChanges); @@ -391,9 +391,9 @@ public sealed class SettingsVMTests [Fact] public void Cancel_reverts_audio_draft_to_persisted() { - var custom = AudioSettings.Default with { Music = 0.2f }; + var custom = AudioSettings.Default with { Ambient = 0.2f }; var (vm, _, _, _, _, _, _, _, _, _) = Build(persistedAudio: custom); - vm.SetAudio(vm.AudioDraft with { Music = 0.9f, Master = 0.3f }); + vm.SetAudio(vm.AudioDraft with { Ambient = 0.9f, Master = 0.3f }); Assert.True(vm.HasUnsavedChanges); vm.Cancel();