MONKEY (reviewfix-a): cloud-review fixes — cvars, capture fixture, wind pin, post perf

- farclip High seed 777 declared in SEEDED_DEVIATIONS (owner: High is the
  default); test walks the seeded column against every reference row (#6)
- WOW_CAPTURE_CVARS=<file> replaces BENILLA_HOME as the capture cvar fixture (#13)
- label derivation skips session-owned rows, so an env lever no longer
  persists graphicsQuality/lightingQuality = Custom (#14)
- /console help strings: restored line continuations + a test (#15)
- WOW_FOLIAGE_WIND read only in a dev build under WOW_CAPTURE (#16)
- fog node skipped at volumetricFog 0 with no lamp; fog and SSAO uniforms
  persistent per view, written in prepare (#17)
- tests: every governed graphics row through its observer; OptionsFrame
  farclip slider tied to view::FARCLIP_RANGE

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pkuzic
2026-09-26 00:42:27 +01:00
co-authored by Claude Opus 5.5
parent 40238ad3e8
commit b2b5ef7539
7 changed files with 310 additions and 47 deletions
+176 -21
View File
@@ -955,7 +955,8 @@ pub(crate) const REGISTERED: &[Registered] = &[
ours(
"skyQuality",
"0",
"benilla's own: sky quality, 0 Classic / 1 Enhanced (smooth gradient, sun glow, stars) / \n 2 High (+ detailed sun-lit clouds)",
"benilla's own: sky quality, 0 Classic / 1 Enhanced (smooth gradient, sun glow, stars) / \
2 High (+ detailed sun-lit clouds)",
),
// MONKEY (wind): one tier controls the grass-only and grass-plus-tree receivers.
ours(
@@ -984,7 +985,8 @@ pub(crate) const REGISTERED: &[Registered] = &[
ours(
"zoneSkyboxes",
"0",
"benilla's own: draw the zone skybox LightParams names for the living, 0 Off / 1 On; the reference draws a DBC skybox only for the ghost",
"benilla's own: draw the zone skybox LightParams names for the living, 0 Off / 1 On; the \
reference draws a DBC skybox only for the ghost",
),
ours(
"waterQuality",
@@ -1133,13 +1135,15 @@ pub(crate) const REGISTERED: &[Registered] = &[
ours(
"exteriorShadows",
"1",
"benilla's own: outdoor fire lights (campfires, braziers, lampposts) cast real shadows at night; no effect by day",
"benilla's own: outdoor fire lights (campfires, braziers, lampposts) cast real shadows at \
night; no effect by day",
),
// MONKEY (daylight: terrain torch casters): the ground as a torch caster.
ours(
"torchTerrainShadows",
"0",
"benilla's own: the ground casts into outdoor fire shadows (hills and banks block a fire's light); needs exteriorShadows",
"benilla's own: the ground casts into outdoor fire shadows (hills and banks block a fire's \
light); needs exteriorShadows",
),
// MONKEY (static torch cache): residency and per-frame work have separate live budgets.
ours(
@@ -2334,8 +2338,11 @@ fn same_value(a: &str, b: &str) -> bool {
/// allowed to say, which is what keeps the row from going stale behind a `/console` write.
pub(crate) fn derive_lighting_quality(cvars: &Cvars) -> &'static str {
for (name, members) in LIGHTING_PRESETS {
// MONKEY (reviewfix-a): a session-owned row (an env lever) is not the player's choice and
// must not turn the saved label Custom; it is skipped, as the seed skips it.
let matched = members
.iter()
.filter(|(k, _)| !cvars.is_session_owned(k))
.all(|(k, v)| cvars.get(k).is_some_and(|live| same_value(live, v)));
if matched {
return name;
@@ -2379,6 +2386,21 @@ pub(crate) const GRAPHICS_PRESET_NAMES: [&str; 5] = ["Classic", "Low", "Medium",
/// The rung a player with no saved preset boots into ([`Cvars::seed_graphics_preset`]).
pub(crate) const GRAPHICS_DEFAULT: &str = "High";
/// MONKEY (reviewfix-a): **the rows where the first-boot seed leaves the reference** — the
/// [`GRAPHICS_DEFAULT`] column value a new player actually boots with, against the reference's
/// own default. The registered row keeps its `same(...)` (the registry default IS the
/// reference's; a capture or a test, which never seeds, runs at it), so this list is where the
/// deviation of the *seeded* boot value is declared, `Deviates`-style: `(row, reference, why)`.
/// `seeded_column_deviates_only_where_declared` walks the whole column against the reference
/// rows, both ways.
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) const SEEDED_DEVIATIONS: &[(&str, &str, &str)] = &[(
"farclip",
"350",
"owner decision: the High preset is the first-boot default, and High's view distance is the \
reference's own slider maximum, 777 yd; Classic keeps 350",
)];
/// **The one place the Graphics Preset is written down**: one line per governed row, one column
/// per rung of [`GRAPHICS_PRESET_NAMES`]. `lightingQuality` is itself a row — writing its rung
/// writes [`LIGHTING_PRESETS`]' members — so every lighting, water, fog, post and wind row the
@@ -2421,8 +2443,10 @@ fn graphics_column(name: &str) -> Option<usize> {
/// ladders show). Reads `lightingQuality`'s DERIVED label, so run it after that is mirrored.
pub(crate) fn derive_graphics_quality(cvars: &Cvars) -> &'static str {
for (col, name) in GRAPHICS_PRESET_NAMES.iter().enumerate() {
// MONKEY (reviewfix-a): session-owned rows are skipped, as in the lighting ladder above.
let matched = GRAPHICS_PRESETS
.iter()
.filter(|(k, _)| !cvars.is_session_owned(k))
.all(|(k, v)| cvars.get(k).is_some_and(|live| same_value(live, v[col])));
if matched {
return name;
@@ -2636,14 +2660,14 @@ fn stored_config() -> StoredConfig {
}
}
/// MONKEY (integration): a capture may opt into one explicit, read-only CVar fixture through
/// `BENILLA_HOME`. All other local state stays hermetic and [`save_config`] still sees no path,
/// so deterministic A/B homes can select graphics tiers without a capture writing anything back.
/// MONKEY (integration): a capture may opt into one explicit, read-only CVar fixture.
/// MONKEY (reviewfix-a): through its own variable, `WOW_CAPTURE_CVARS=<config.toml path>`, not
/// `BENILLA_HOME` (the general local-state override a player shell may carry, which would load
/// that player's settings into a baseline). Without it a capture reads no config at all. All
/// other local state stays hermetic and [`save_config`] still sees no path.
fn config_read_path() -> Option<std::path::PathBuf> {
if std::env::var_os("WOW_CAPTURE").is_some() {
return std::env::var_os("BENILLA_HOME")
.map(std::path::PathBuf::from)
.map(|home| home.join("config.toml"));
return std::env::var_os("WOW_CAPTURE_CVARS").map(std::path::PathBuf::from);
}
crate::local_state::config_path()
}
@@ -3740,19 +3764,36 @@ mod tests {
/// row's rung values do too.
#[test]
fn graphics_rows_pass_their_observers_unclamped() {
// MONKEY (reviewfix-a): every governed row, every rung — not only `farclip`. Each value
// is driven through the real setter and read back off the resource its renderer reads.
let mut app = cvar_app();
for (col, name) in GRAPHICS_PRESET_NAMES.iter().enumerate() {
let farclip = GRAPHICS_PRESETS
.iter()
.find(|(k, _)| *k == "farclip")
.unwrap()
.1[col];
apply(&mut app, "farclip", farclip);
assert_eq!(
res::<ViewDistance>(&app).farclip,
farclip.parse::<f32>().unwrap(),
"{name}"
);
for (k, values) in GRAPHICS_PRESETS {
if *k == "lightingQuality" {
continue; // its members are welded by the lighting-preset census test
}
let value = values[col];
apply(&mut app, k, "-1");
apply(&mut app, k, value);
let video = res::<VideoConfig>(&app);
let flag = |b: bool| if b { 1.0 } else { 0.0 };
let applied = match *k {
"farclip" => res::<ViewDistance>(&app).farclip,
"skyQuality" => video.sky_quality as f32,
"skyDither" => flag(res::<benilla_world::ffx_glow::SkyDither>(&app).0),
"fogModel" => {
flag(res::<benilla_world::lighting::FogModelSetting>(&app).modern())
}
"rainSurfaces" => flag(res::<benilla_world::weather::RainSurfaces>(&app).0),
"torchTerrainShadows" => flag(video.torch_terrain_shadows),
"daylightWindowSplit" => flag(video.daylight_window_split),
"ambientOcclusion" => video.ambient_occlusion as f32,
"zoneSkyboxes" => flag(res::<benilla_world::skybox::ZoneSkyboxes>(&app).0),
"lampFog" => video.lamp_fog as f32,
_ => panic!("add the observer readback for {k}"),
};
assert_eq!(applied, value.parse::<f32>().unwrap(), "{name}/{k}");
}
}
}
@@ -4382,6 +4423,12 @@ mod tests {
app.init_resource::<benilla_world::weather::RainSurfaces>(); // MONKEY (wet)
app.add_observer(crate::monkey_gfx::on_cvar);
},
// MONKEY (reviewfix-a): the zone-skybox bridge, so every governed row has its observer.
|app| {
app.insert_resource(crate::zone_skybox::ZoneSkyboxOverride(None));
app.init_resource::<benilla_world::skybox::ZoneSkyboxes>();
app.add_observer(crate::zone_skybox::on_cvar);
},
|app| {
app.add_observer(crate::player::camera::on_cvar);
},
@@ -4957,6 +5004,8 @@ mod tests {
)
.unwrap();
let _h = EnvGuard::set("BENILLA_HOME", tmp.to_str().unwrap());
// MONKEY (reviewfix-a): the fixture is named by its own variable, not BENILLA_HOME.
let _x = EnvGuard::set("WOW_CAPTURE_CVARS", tmp.join("config.toml").to_str().unwrap());
let _c = EnvGuard::set("WOW_CAPTURE", "post-lava-searing");
assert_eq!(boot_cvar("bloom").as_deref(), Some("0"));
@@ -5073,4 +5122,110 @@ mod tests {
]
);
}
// ─── MONKEY (reviewfix-a) ────────────────────────────────────────────────────────────────
/// The value the reference boots a row at, as its [`Reference`] column records it; `None`
/// for benilla's own rows (nothing to match).
fn reference_boot_value(row: &Registered) -> Option<&'static str> {
match &row.reference {
Reference::Same(v) => Some(v),
// The reference's own boot code lands where our default does.
Reference::Overridden { .. } => Some(row.default),
Reference::Deviates { value, .. } => Some(value),
Reference::Ours(_) => None,
}
}
/// **The seeded High column against every reference row** (review #6): a governed row a new
/// player boots off the reference's value must be declared in [`SEEDED_DEVIATIONS`] with
/// that reference value and a reason, and a declared row must really deviate.
#[test]
fn seeded_column_deviates_only_where_declared() {
let col = graphics_column(GRAPHICS_DEFAULT).unwrap();
for (k, values) in GRAPHICS_PRESETS {
if k.eq_ignore_ascii_case("lightingQuality") {
continue; // High on that ladder IS the registered defaults (its own test)
}
let row = REGISTERED.iter().find(|r| r.name == *k).expect("governed row registered");
let declared = SEEDED_DEVIATIONS.iter().find(|(n, _, _)| n == k);
match reference_boot_value(row) {
Some(reference) if !same_value(values[col], reference) => {
let (_, value, why) = declared.unwrap_or_else(|| {
panic!(
"{k}: the seeded {GRAPHICS_DEFAULT} value {} leaves the reference's \
{reference}; declare it in SEEDED_DEVIATIONS",
values[col]
)
});
assert!(same_value(value, reference), "{k}: declared reference is stale");
assert!(!why.trim().is_empty(), "{k}: a deviation owes a reason");
}
_ => assert!(
declared.is_none(),
"{k}: declared in SEEDED_DEVIATIONS but the seed does not deviate"
),
}
}
for (n, _, _) in SEEDED_DEVIATIONS {
assert!(
GRAPHICS_PRESETS.iter().any(|(k, _)| k == n),
"{n}: declared but not governed"
);
}
}
/// Review #14: an env lever owns a governed row for the session; the label must not derive
/// (and so persist) `Custom` because of it.
#[test]
fn a_session_owned_row_does_not_derive_custom() {
let mut cvars = fresh_registry();
apply_graphics_preset(&mut cvars, "High");
lighting_quality(&mut cvars);
assert_eq!(derive_graphics_quality(&cvars), "High");
cvars.own_for_session("farclip", Some("500"));
assert_eq!(cvars.get("farclip"), Some("500"));
assert_eq!(derive_graphics_quality(&cvars), "High");
// A lighting member owned by the session is skipped the same way.
cvars.own_for_session("waterQuality", Some("0"));
assert_eq!(derive_lighting_quality(&cvars), "High");
}
/// Review #15: help strings are one line — a lost `\` continuation leaves a literal `\n` or a
/// run of indentation spaces in the `/console` text.
#[test]
fn help_strings_have_no_broken_continuations() {
for row in REGISTERED {
let why = match &row.reference {
Reference::Ours(why)
| Reference::Deviates { why, .. }
| Reference::Overridden { why, .. } => *why,
Reference::Same(_) => continue,
};
assert!(
!why.contains('\n') && !why.contains("\\n") && !why.contains(" "),
"{}: broken line continuation in {why:?}",
row.name
);
}
}
/// Review #13: a capture reads CVars only from the dedicated `WOW_CAPTURE_CVARS` fixture,
/// never from a `BENILLA_HOME` the shell happens to carry.
#[test]
fn a_capture_reads_cvars_only_from_its_fixture() {
use crate::local_state::test_env::{EnvGuard, ENV_LOCK};
let _l = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let _c = EnvGuard::set("WOW_CAPTURE", "overlook-noon");
let _h = EnvGuard::set("BENILLA_HOME", "player-home");
let _x = EnvGuard::unset("WOW_CAPTURE_CVARS");
assert_eq!(config_read_path(), None);
let _x = EnvGuard::set("WOW_CAPTURE_CVARS", "fixture/config.toml");
assert_eq!(
config_read_path(),
Some(std::path::PathBuf::from("fixture/config.toml"))
);
}
}
+16 -7
View File
@@ -33,12 +33,9 @@ pub(crate) fn on_cvar(
}
// MONKEY (wind): numeric tier, live; 0 is the exact no-displacement path.
"foliagewind" => {
// Capture-only A/B override. Normal runs have no such environment variable and obey
// the live CVar exactly; the instrument can force Off without rewriting Config.wtf.
let requested = std::env::var("WOW_FOLIAGE_WIND")
.ok()
.and_then(|v| v.parse::<f32>().ok())
.unwrap_or_else(|| ev.num());
// Capture-only A/B pin (MONKEY reviewfix-a: gated like `capture_daylight` — dev
// build AND `WOW_CAPTURE`). Player runs obey the live CVar exactly.
let requested = foliage_wind_pin().map(f32::from).unwrap_or_else(|| ev.num());
let want = requested.clamp(0.0, 2.0) as u8;
if foliage_wind.0 != want {
foliage_wind.0 = want;
@@ -57,6 +54,14 @@ pub(crate) fn on_cvar(
}
}
/// MONKEY (reviewfix-a): the `WOW_FOLIAGE_WIND` capture pin, only in a dev build under `WOW_CAPTURE`.
fn foliage_wind_pin() -> Option<u8> {
if !crate::run_mode::dev_affordances() {
return None;
}
FoliageWind::capture_override()
}
/// Registers the programme's cvar bridges. Must be added before `CvarPlugin` so the saved values
/// applied at Startup reach it (the edge `game_plugins.rs` documents for every knob plugin).
pub(crate) struct MonkeyGfxPlugin;
@@ -64,7 +69,11 @@ pub(crate) struct MonkeyGfxPlugin;
impl Plugin for MonkeyGfxPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<SkyDither>()
.init_resource::<FoliageWind>()
// MONKEY (reviewfix-a): the resource is seeded here so a player build never takes the
// capture pin (the world plugin's `init_resource` keeps whatever is already present).
.insert_resource(FoliageWind(
foliage_wind_pin().unwrap_or(FoliageWind::REGISTERED),
))
.init_resource::<FogModelSetting>()
.init_resource::<RainSurfaces>() // MONKEY (wet)
.add_observer(on_cvar);
+29 -4
View File
@@ -129,6 +129,8 @@ impl Plugin for AmbientOcclusionPlugin {
(
prepare_pipelines.in_set(RenderSystems::Prepare),
prepare_textures.in_set(RenderSystems::PrepareResources),
// MONKEY (reviewfix-a): one uniform per view, rewritten, not allocated per frame.
prepare_ao_uniforms.in_set(RenderSystems::PrepareResources),
),
)
.add_render_graph_node::<ViewNodeRunner<AoNode>>(Core3d, AoLabel)
@@ -357,6 +359,31 @@ fn prepare_textures(
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
struct AoLabel;
/// MONKEY (reviewfix-a): the view's AO uniform, created once and rewritten in prepare.
#[derive(Component)]
struct AoUniform(UniformBuffer<AoView>);
fn prepare_ao_uniforms(
mut commands: Commands,
device: Res<RenderDevice>,
queue: Res<RenderQueue>,
mut views: Query<(Entity, &AoView, Option<&mut AoUniform>)>,
) {
for (entity, settings, uniform) in &mut views {
match uniform {
Some(mut uniform) => {
uniform.0.set(*settings);
uniform.0.write_buffer(&device, &queue);
}
None => {
let mut uniform = UniformBuffer::from(*settings);
uniform.write_buffer(&device, &queue);
commands.entity(entity).insert(AoUniform(uniform));
}
}
}
}
#[derive(Default)]
struct AoNode;
@@ -365,7 +392,7 @@ impl ViewNode for AoNode {
&'static ViewTarget,
&'static ViewDepthTexture,
&'static ViewUniformOffset,
&'static AoView,
&'static AoUniform,
&'static ViewAoPipelines,
&'static AoTextures,
);
@@ -397,9 +424,7 @@ impl ViewNode for AoNode {
let layouts = world.resource::<AoPipeline>();
let ms = (depth.texture.sample_count() > 1) as usize;
let device = context.render_device().clone();
let mut uniform = UniformBuffer::from(*settings);
uniform.write_buffer(&device, world.resource::<RenderQueue>());
let params = uniform.binding().unwrap();
let params = settings.0.binding().unwrap();
// `main_texture_view` is the single-sample texture the opaque pass resolved into.
let ao_bind = device.create_bind_group(
@@ -5732,3 +5732,29 @@ fn the_guild_line_greys_with_player_names_and_the_follow_speed_with_the_style()
assert!(speed_thumb(&mut s));
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// MONKEY (reviewfix-a): the Advanced Graphics farclip slider's range is `view::FARCLIP_RANGE`,
/// and its 60-yd grid lands exactly on the maximum (the 1.12 grid the page advertises).
#[test]
fn farclip_slider_range_is_the_view_range() {
let xml = include_str!("../../assets/ui/OptionsFrame.xml");
let row = xml
.split("<Frame name=\"$parentRowRenderDistance\"")
.nth(1)
.expect("the farclip row");
let args = row
.split("OptionsSliderRow_Setup(self, ")
.nth(1)
.and_then(|rest| rest.split(')').next())
.expect("the slider setup call");
let nums: Vec<f32> = args
.split(',')
.take(3)
.map(|v| v.trim().parse().expect("numeric slider bound"))
.collect();
let range = benilla_world::view::FARCLIP_RANGE;
assert_eq!(nums[0], *range.start(), "slider min");
assert_eq!(nums[1], benilla_world::view::FARCLIP_MAX, "slider max");
assert_eq!(nums[1], *range.end());
assert_eq!((nums[1] - nums[0]) % nums[2], 0.0, "the step lands on the maximum");
}
+41 -5
View File
@@ -135,7 +135,14 @@ impl Plugin for VolumetricFogPlugin {
.insert_resource(FogShader(shader))
.init_resource::<SpecializedRenderPipelines<FogPipeline>>()
.add_systems(RenderStartup, init_pipeline)
.add_systems(Render, prepare_pipelines.in_set(RenderSystems::Prepare))
.add_systems(
Render,
(
prepare_pipelines.in_set(RenderSystems::Prepare),
// MONKEY (reviewfix-a): one uniform per view, rewritten, not allocated per frame.
prepare_fog_uniforms.in_set(RenderSystems::PrepareResources),
),
)
.add_render_graph_node::<ViewNodeRunner<FogNode>>(Core3d, FogLabel)
.add_render_graph_edges(
Core3d,
@@ -289,6 +296,12 @@ fn update_fog(
} else {
FogLamps::default()
};
// MONKEY (reviewfix-a): no haze tier and no lamp in range is an identity pass — skip the
// node (and its depth bind and ping-pong) rather than run it every frame.
if tier == 0 && lamps.count == 0 {
commands.entity(entity).remove::<FogView>();
continue;
}
let fog = Vec3::from_array(lighting.fog_color);
// MONKEY (fog): rows 1-3 as packed for the light buffer.
let rows = monkey.as_ref().map_or([[0.0; 4]; 16], |m| m.pack(0.0, 0.0));
@@ -323,6 +336,31 @@ fn update_fog(
#[derive(Resource)]
struct FogShader(Handle<Shader>);
/// MONKEY (reviewfix-a): the view's fog uniform, created once and rewritten in prepare.
#[derive(Component)]
struct FogUniform(UniformBuffer<FogView>);
fn prepare_fog_uniforms(
mut commands: Commands,
device: Res<RenderDevice>,
queue: Res<RenderQueue>,
mut views: Query<(Entity, &FogView, Option<&mut FogUniform>)>,
) {
for (entity, fog, uniform) in &mut views {
match uniform {
Some(mut uniform) => {
uniform.0.set(*fog);
uniform.0.write_buffer(&device, &queue);
}
None => {
let mut uniform = UniformBuffer::from(*fog);
uniform.write_buffer(&device, &queue);
commands.entity(entity).insert(FogUniform(uniform));
}
}
}
}
#[derive(Resource)]
struct FogPipeline {
layouts: [BindGroupLayoutDescriptor; 2],
@@ -441,7 +479,7 @@ impl ViewNode for FogNode {
&'static ViewUniformOffset,
&'static ViewLightsUniformOffset,
&'static ViewShadowBindings,
&'static FogView,
&'static FogUniform,
&'static ViewFogPipeline,
);
fn run<'w>(
@@ -474,8 +512,6 @@ impl ViewNode for FogNode {
};
let settings = world.resource::<FogPipeline>();
let device = context.render_device();
let mut uniform = UniformBuffer::from(*fog);
uniform.write_buffer(device, world.resource::<RenderQueue>());
let out = target.post_process_write();
let bind = device.create_bind_group(
"gamma_fog",
@@ -489,7 +525,7 @@ impl ViewNode for FogNode {
lights,
&shadows.directional_light_depth_texture_view,
&settings.comparison,
uniform.binding().unwrap(),
fog.0.binding().unwrap(),
)),
);
let diagnostics = context.diagnostic_recorder();
+2 -2
View File
@@ -6,7 +6,7 @@ use bevy::prelude::*;
/// The session override, read once.
#[derive(Resource, Clone, Copy)]
struct ZoneSkyboxOverride(Option<bool>);
pub(crate) struct ZoneSkyboxOverride(pub(crate) Option<bool>); // MONKEY (reviewfix-a): test census
pub(crate) struct ZoneSkyboxPlugin;
@@ -31,7 +31,7 @@ fn apply_override(
}
}
fn on_cvar(
pub(crate) fn on_cvar(
ev: On<crate::cvars::CvarChanged>,
forced: Res<ZoneSkyboxOverride>,
zone: Option<ResMut<benilla_world::skybox::ZoneSkyboxes>>,
+20 -8
View File
@@ -74,17 +74,29 @@ impl Default for WindField {
#[derive(Resource, Clone, Copy, Debug, PartialEq, Eq)]
pub struct FoliageWind(pub u8);
impl FoliageWind {
/// The registered `foliageWind` default (grass + trees).
pub const REGISTERED: u8 = 2;
/// MONKEY (reviewfix-a): the capture-only A/B pin `WOW_FOLIAGE_WIND`, read only under
/// `WOW_CAPTURE` (like the other capture levers) so a player run always obeys the saved CVar.
/// The app adds the dev-build gate on top.
pub fn capture_override() -> Option<u8> {
std::env::var_os("WOW_CAPTURE")?;
std::env::var("WOW_FOLIAGE_WIND")
.ok()
.and_then(|v| v.trim().parse::<f32>().ok())
.filter(|v| v.is_finite())
.map(|v| v.clamp(0.0, 2.0) as u8)
}
}
impl Default for FoliageWind {
fn default() -> Self {
// The resource exists before the CVar host. Its ordinary default therefore has to equal
// the registered default; a default-valued CVar emits no change event at boot. The env
// arm is capture-only and lets the A/B harness force the exact zero-displacement path.
let tier = std::env::var("WOW_FOLIAGE_WIND")
.ok()
.and_then(|v| v.parse::<f32>().ok())
.unwrap_or(2.0)
.clamp(0.0, 2.0) as u8;
Self(tier)
// the registered default; a default-valued CVar emits no change event at boot. The
// capture pin lets the A/B harness force the exact zero-displacement path.
Self(Self::capture_override().unwrap_or(Self::REGISTERED))
}
}