diff --git a/crates/benilla-app/src/cvars.rs b/crates/benilla-app/src/cvars.rs index 98482d6c..850311eb 100644 --- a/crates/benilla-app/src/cvars.rs +++ b/crates/benilla-app/src/cvars.rs @@ -2340,6 +2340,9 @@ fn load_config(world: &mut World) { if seeds_graphics_preset(&stored_kind) { cvars.seed_graphics_preset(); } + // Last, so it beats the file and the preset both: the one way to reproduce a player's + // setting in a capture, which is otherwise hermetic and boots on the registered defaults. + apply_env_overrides(&mut cvars); cvars.take_events() }; for event in events { @@ -2347,6 +2350,31 @@ fn load_config(world: &mut World) { } } +/// `WOW_CVAR==[;=…]`, applied after the file and the preset so it wins. +/// +/// A capture reads no `config.toml` — that is what keeps an A/B exact — so without this there is +/// no way to ask "does it still look wrong at *his* settings?" offline. Unset, it does nothing, +/// and every capture keeps booting on the registered defaults. +fn apply_env_overrides(cvars: &mut Cvars) { + let Ok(script) = std::env::var("WOW_CVAR") else { + return; + }; + for step in script.split(';').map(str::trim).filter(|s| !s.is_empty()) { + match step.split_once('=') { + Some((name, value)) => { + let (name, value) = (name.trim(), value.trim()); + match cvars.set(name, value) { + SetOutcome::Changed | SetOutcome::Unchanged | SetOutcome::Staged => { + info!("cvar: WOW_CVAR set {name} = {value}"); + } + outcome => warn!("cvar: WOW_CVAR {name} = {value} refused ({outcome:?})"), + } + } + None => warn!("cvar: WOW_CVAR step {step:?} is not ="), + } + } +} + /// MONKEY (presets): which [`StoredConfig`] arm a boot took, kept past the table's move. enum StoredKind { Absent, @@ -3037,6 +3065,57 @@ mod tests { Cvars::default() } + /// `WOW_CVAR` is how a capture reproduces a player's setting. A capture reads no + /// `config.toml` — that hermetic boot is what keeps an A/B exact — so without this there is no + /// way to ask offline whether a report still reproduces at *their* numbers. Unset it must do + /// nothing at all, or every capture ever taken drifts off the registered defaults. + #[test] + fn the_env_override_beats_the_defaults_and_is_inert_when_unset() { + let _l = crate::local_state::test_env::ENV_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + + let mut untouched = fresh_registry(); + { + let _c = crate::local_state::test_env::EnvGuard::unset("WOW_CVAR"); + apply_env_overrides(&mut untouched); + } + assert_eq!( + untouched.take_events().len(), + 0, + "unset, it must not move a single row" + ); + + let mut cvars = fresh_registry(); + let before = cvars.get("interiorGain").map(str::to_string); + assert_eq!(before.as_deref(), Some("0.5"), "the registered default"); + { + let _c = crate::local_state::test_env::EnvGuard::set( + "WOW_CVAR", + "interiorGain=1.25; nightGain=0.9", + ); + apply_env_overrides(&mut cvars); + } + assert_eq!(cvars.get("interiorGain"), Some("1.25")); + assert_eq!(cvars.get("nightGain"), Some("0.9")); + + // An unknown name and a malformed step are reported, not fatal: a typo in a measurement + // must not take the run down with it. + let mut noisy = fresh_registry(); + { + let _c = crate::local_state::test_env::EnvGuard::set( + "WOW_CVAR", + "noSuchCvar=1;interiorGain", + ); + apply_env_overrides(&mut noisy); + } + assert_eq!( + noisy.get("interiorGain"), + Some("0.5"), + "a malformed step changes nothing" + ); + } + /// **The round trip, for every rung**: apply a preset, derive, and get that preset's own name /// back. This is the property the whole ladder rests on — without it the dropdown would show /// `Custom` the instant after the player picked something, which is the exact failure the