diff --git a/crates/benilla-app/assets/ui/CharacterFrameAdapters.xml b/crates/benilla-app/assets/ui/CharacterFrameAdapters.xml
new file mode 100644
index 00000000..7c4e2419
--- /dev/null
+++ b/crates/benilla-app/assets/ui/CharacterFrameAdapters.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/crates/benilla-app/assets/ui/benilla.toc b/crates/benilla-app/assets/ui/benilla.toc
index 9170c73a..c17398e6 100644
--- a/crates/benilla-app/assets/ui/benilla.toc
+++ b/crates/benilla-app/assets/ui/benilla.toc
@@ -121,6 +121,8 @@ Interface\FrameXML\ReputationFrame.xml
Interface\FrameXML\SkillFrame.xml
# Brings HonorFrameTemplates.xml, whose row templates `Blizzard_InspectUI` inherits.
Interface\FrameXML\HonorFrame.xml
+# Ours: the sixth tab page TurtleWoW's `CharacterFrame.lua` lists and 1.12 never shipped.
+CharacterFrameAdapters.xml
Interface\FrameXML\DressUpFrame.xml
Interface\FrameXML\SpellBookFrame.xml
# Ours: the pet page's autocast widgets in the pet action button's proportions.
diff --git a/crates/benilla-app/src/addon_harness/render_tests.rs b/crates/benilla-app/src/addon_harness/render_tests.rs
index d083d5ea..c4d1308d 100644
--- a/crates/benilla-app/src/addon_harness/render_tests.rs
+++ b/crates/benilla-app/src/addon_harness/render_tests.rs
@@ -10,7 +10,9 @@ use super::{survey, Drew};
// so the same oracle runs without administrator symlink privileges on Windows.
pub(super) fn link_fixture(source: &Path, destination: &Path) -> std::io::Result<()> {
#[cfg(unix)]
- { std::os::unix::fs::symlink(source, destination) }
+ {
+ std::os::unix::fs::symlink(source, destination)
+ }
#[cfg(not(unix))]
{
std::fs::create_dir_all(destination)?;
diff --git a/crates/benilla-app/src/blob_shadow.rs b/crates/benilla-app/src/blob_shadow.rs
index 40252fb5..c04bdf16 100644
--- a/crates/benilla-app/src/blob_shadow.rs
+++ b/crates/benilla-app/src/blob_shadow.rs
@@ -41,9 +41,9 @@ use crate::net::{Embodied, NetEntity};
use benilla_world::decal::{DecalFrame, WorldDecal};
// MONKEY (moon shadows): the oval now yields to a NIGHT cast as well as a day one, so it needs the
// same two numbers the packer weights that cast with.
-use benilla_world::lighting::ShadowHandover;
use crate::character_shadow::CharacterShadowReady;
use crate::shadow_core::{ShadowFrame, ShadowSet};
+use benilla_world::lighting::ShadowHandover;
use benilla_world::particles::buffer::{begin_effect_frame, EffectVertex};
use benilla_world::view::WorldCamera;
use benilla_world::wmo_portal::UnitWmoRoom;
@@ -241,9 +241,14 @@ fn update_shadows(
// nothing left to draw, so that stays the early-out the hard hide used to be.
let indoors = room.is_some_and(|r| r.room().is_some());
let mut lane_w = 1.0;
- if video.character_shadows && !indoors
- && shadow_frame.as_ref().is_some_and(|frame| frame.active && !frame.suspended)
- && ready.as_ref().is_some_and(|ready| ready.0.contains(&shadow.owner))
+ if video.character_shadows
+ && !indoors
+ && shadow_frame
+ .as_ref()
+ .is_some_and(|frame| frame.active && !frame.suspended)
+ && ready
+ .as_ref()
+ .is_some_and(|ready| ready.0.contains(&shadow.owner))
{
let cam_dist = camera_pos.map_or(f32::INFINITY, |c| unit.translation.distance(c));
lane_w = blob_weight(lane_strength, cam_dist, video.shadow_distance);
@@ -585,7 +590,10 @@ mod tests {
for step in 0..=20 {
let height = 0.25 - 0.25 * step as f32 / 20.0; // ~14° down to the horizon
let w = blob_weight(sun_shadow_strength(height), 10.0, 70.0);
- assert!(w >= prev - 1e-6, "dusk weight went backwards at step {step}");
+ assert!(
+ w >= prev - 1e-6,
+ "dusk weight went backwards at step {step}"
+ );
assert!(w - prev < 0.35, "dusk weight jumped at step {step}");
prev = w;
}
@@ -609,7 +617,10 @@ mod tests {
let night_sun = sun_shadow_strength(-0.3);
assert_eq!(night_sun, 0.0);
let full = blob_weight(night_sun, 10.0, 70.0);
- assert_eq!(full, 1.0, "with the feature off the whole oval is still there");
+ assert_eq!(
+ full, 1.0,
+ "with the feature off the whole oval is still there"
+ );
let under_moon = blob_weight(night_sun + 0.35, 10.0, 70.0);
assert!(
(under_moon - 0.65).abs() < 1e-6,
@@ -625,7 +636,10 @@ mod tests {
let mut prev = 1.0;
for step in 0..=20 {
let w = blob_weight(night_sun + 0.35 * step as f32 / 20.0, 10.0, 70.0);
- assert!(w <= prev + 1e-6, "the oval brightened as the moon rose, at step {step}");
+ assert!(
+ w <= prev + 1e-6,
+ "the oval brightened as the moon rose, at step {step}"
+ );
prev = w;
}
assert!((prev - 0.65).abs() < 1e-6);
diff --git a/crates/benilla-app/src/capture/fixtures.rs b/crates/benilla-app/src/capture/fixtures.rs
index 4ee9c272..0cd39407 100644
--- a/crates/benilla-app/src/capture/fixtures.rs
+++ b/crates/benilla-app/src/capture/fixtures.rs
@@ -1044,7 +1044,9 @@ pub(super) fn seed_ui_fixture(
}
// MONKEY (volumetric fog): exercise the rendered Advanced Graphics dropdown and layout.
UiFixture::OptionsAdvancedGraphics => {
- let Some(script) = script else { return; };
+ let Some(script) = script else {
+ return;
+ };
script.register_cvars(crate::cvars::registered_pairs());
if let Err(e) = script.run(
"ShowUIPanel(BenillaOptionsFrame); BenillaOptionsFrameCategoryListRowAdvancedGraphics:Click()",
@@ -1685,7 +1687,10 @@ pub(super) fn seed_perf_crowd(
spatial: avian3d::prelude::SpatialQuery,
mut seeded: Local,
) {
- let Some(n) = std::env::var("WOW_PERF_CROWD").ok().and_then(|v| v.parse::().ok()) else {
+ let Some(n) = std::env::var("WOW_PERF_CROWD")
+ .ok()
+ .and_then(|v| v.parse::().ok())
+ else {
return;
};
let Some(scenario) = ctx.scenario else {
diff --git a/crates/benilla-app/src/capture/scenarios.rs b/crates/benilla-app/src/capture/scenarios.rs
index 1d6ab8ab..9ddef204 100644
--- a/crates/benilla-app/src/capture/scenarios.rs
+++ b/crates/benilla-app/src/capture/scenarios.rs
@@ -289,29 +289,48 @@ pub(super) const SUBJECT_INDOOR: [f32; 3] = [-9469.4, 31.9, 57.9];
pub(super) const ON_DEMAND: &[Scenario] = &[
// MONKEY (volumetric fog): UI proof uses the same live page selection as a player.
Scenario {
- name: "ui-options-advanced", map: Some(MAP_AZEROTH),
- eye: GROUND_EYE, look: GROUND_LOOK, minute: 720,
+ name: "ui-options-advanced",
+ map: Some(MAP_AZEROTH),
+ eye: GROUND_EYE,
+ look: GROUND_LOOK,
+ minute: 720,
ui: Some(UiFixture::OptionsAdvancedGraphics),
},
// MONKEY (volumetric fog): reproducible dawn trees, moonlit lamps and the inn's common room.
Scenario {
- name: "volfog-dawn", map: Some(MAP_AZEROTH),
- eye: [-9460.0, 70.0, 58.0], look: [-9410.0, 120.0, 67.0], minute: 390, ui: None,
+ name: "volfog-dawn",
+ map: Some(MAP_AZEROTH),
+ eye: [-9460.0, 70.0, 58.0],
+ look: [-9410.0, 120.0, 67.0],
+ minute: 390,
+ ui: None,
},
Scenario {
- name: "volfog-night", map: Some(MAP_AZEROTH),
- eye: [-9460.0, 70.0, 58.0], look: [-9450.0, 20.0, 61.0], minute: 0, ui: None,
+ name: "volfog-night",
+ map: Some(MAP_AZEROTH),
+ eye: [-9460.0, 70.0, 58.0],
+ look: [-9450.0, 20.0, 61.0],
+ minute: 0,
+ ui: None,
},
// MONKEY (volumetric fog): celestial_sun_direction(420) has WoW azimuth
// 45 degrees and elevation 12.20778 degrees: dz = hypot(70,70)*tan(elevation).
// The Goldshire lake bank has sunlit air behind the gaps between the trees.
Scenario {
- name: "volfog-sun", map: Some(MAP_AZEROTH),
- eye: WATER_EYE, look: [-9457.0, -240.6, 92.21753], minute: 420, ui: None,
+ name: "volfog-sun",
+ map: Some(MAP_AZEROTH),
+ eye: WATER_EYE,
+ look: [-9457.0, -240.6, 92.21753],
+ minute: 420,
+ ui: None,
},
Scenario {
- name: "volfog-inn", map: Some(MAP_AZEROTH),
- eye: INN_EYE, look: INN_LOOK, minute: 390, ui: None,
+ name: "volfog-inn",
+ map: Some(MAP_AZEROTH),
+ eye: INN_EYE,
+ look: INN_LOOK,
+ minute: 390,
+ ui: None,
},
// WOW_CAPTURE_WATER_T=: fixed Enhanced water phase (read once, default 0).
// Compare 0 and 1.5; Classic and non-water animation stay frozen.
@@ -1090,47 +1109,79 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
look: LAVA_RIVER_LOOK,
minute: 0,
ui: None,
- }, // MONKEY (daylight): city interiors by day and terrain torch shadows at night. World coords
+ }, // MONKEY (daylight): city interiors by day and terrain torch shadows at night. World coords
// from the census (`lighting::daylight::census`, Stormwind uid 10047, Ironforge uid 7706).
// The Gilded Rose ground floor (group g268, portal + aperture seeds).
Scenario {
- name: "daylight-sw-inn", map: Some(MAP_AZEROTH),
- eye: [-8871.0, 681.0, 99.8], look: [-8858.0, 668.0, 98.6], minute: 720, ui: None,
+ name: "daylight-sw-inn",
+ map: Some(MAP_AZEROTH),
+ eye: [-8871.0, 681.0, 99.8],
+ look: [-8858.0, 668.0, 98.6],
+ minute: 720,
+ ui: None,
},
// The Cathedral of Light nave (groups g135/g146, lit only by two EXT-class window batches).
Scenario {
- name: "daylight-sw-cathedral", map: Some(MAP_AZEROTH),
- eye: [-8556.0, 826.0, 109.0], look: [-8515.0, 862.0, 112.0], minute: 720, ui: None,
+ name: "daylight-sw-cathedral",
+ map: Some(MAP_AZEROTH),
+ eye: [-8556.0, 826.0, 109.0],
+ look: [-8515.0, 862.0, 112.0],
+ minute: 720,
+ ui: None,
},
// A Trade District house (group g68 NEH02, one exterior portal).
Scenario {
- name: "daylight-sw-shop", map: Some(MAP_AZEROTH),
- eye: [-8799.0, 696.0, 104.3], look: [-8786.0, 707.0, 103.0], minute: 720, ui: None,
+ name: "daylight-sw-shop",
+ map: Some(MAP_AZEROTH),
+ eye: [-8799.0, 696.0, 104.3],
+ look: [-8786.0, 707.0, 103.0],
+ minute: 720,
+ ui: None,
},
// Ironforge: the Great Forge hall (g64, no opening to the sky) and the gate hall (g7, the
// city's one exterior portal).
Scenario {
- name: "daylight-if-forge", map: Some(MAP_AZEROTH),
- eye: [-4930.0, -945.0, 503.5], look: [-4890.0, -985.0, 503.0], minute: 720, ui: None,
+ name: "daylight-if-forge",
+ map: Some(MAP_AZEROTH),
+ eye: [-4930.0, -945.0, 503.5],
+ look: [-4890.0, -985.0, 503.0],
+ minute: 720,
+ ui: None,
},
Scenario {
- name: "daylight-if-gate", map: Some(MAP_AZEROTH),
- eye: [-4975.0, -895.0, 503.5], look: [-5005.0, -852.0, 506.0], minute: 720, ui: None,
- }, // Goldshire's lamps at midnight: terrain receiving (and, with `torchTerrainShadows`, casting)
+ name: "daylight-if-gate",
+ map: Some(MAP_AZEROTH),
+ eye: [-4975.0, -895.0, 503.5],
+ look: [-5005.0, -852.0, 506.0],
+ minute: 720,
+ ui: None,
+ }, // Goldshire's lamps at midnight: terrain receiving (and, with `torchTerrainShadows`, casting)
// torch cube shadows.
Scenario {
- name: "daylight-torch-goldshire", map: Some(MAP_AZEROTH),
- eye: [-9460.0, 70.0, 58.0], look: [-9450.0, 20.0, 61.0], minute: 0, ui: None,
- }, // Two Elwynn road lampposts whose own light the terrain blocks most (the census's
+ name: "daylight-torch-goldshire",
+ map: Some(MAP_AZEROTH),
+ eye: [-9460.0, 70.0, 58.0],
+ look: [-9450.0, 20.0, 61.0],
+ minute: 0,
+ ui: None,
+ }, // Two Elwynn road lampposts whose own light the terrain blocks most (the census's
// `lamp_terrain_occlusion_scan`: 18 % and 16 % of the ground within 25 yd), at midnight, framed
// across the lamp toward the blocked side.
Scenario {
- name: "daylight-torch-hill-a", map: Some(MAP_AZEROTH),
- eye: [-9314.8, 134.1, 70.0], look: [-9320.4, 165.6, 64.0], minute: 0, ui: None,
+ name: "daylight-torch-hill-a",
+ map: Some(MAP_AZEROTH),
+ eye: [-9314.8, 134.1, 70.0],
+ look: [-9320.4, 165.6, 64.0],
+ minute: 0,
+ ui: None,
},
Scenario {
- name: "daylight-torch-hill-b", map: Some(MAP_AZEROTH),
- eye: [-9163.1, 181.1, 77.0], look: [-9147.1, 153.4, 71.0], minute: 0, ui: None,
+ name: "daylight-torch-hill-b",
+ map: Some(MAP_AZEROTH),
+ eye: [-9163.1, 181.1, 77.0],
+ look: [-9147.1, 153.4, 71.0],
+ minute: 0,
+ ui: None,
},
// MONKEY (post): lane-specific A/B and perf viewpoints. Kept at the end for merge isolation.
Scenario {
@@ -1171,62 +1222,110 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
// scenes above). Raw WoW coords, map per scenario; noon = 720, dusk = 1170, night = 0.
// Elwynn from the Goldshire road toward the lake and the forest: sky, fog and trees.
Scenario {
- name: "elwynn-noon", map: Some(MAP_AZEROTH),
- eye: GFX_ELWYNN_EYE, look: GFX_ELWYNN_LOOK, minute: 720, ui: None,
+ name: "elwynn-noon",
+ map: Some(MAP_AZEROTH),
+ eye: GFX_ELWYNN_EYE,
+ look: GFX_ELWYNN_LOOK,
+ minute: 720,
+ ui: None,
},
Scenario {
- name: "elwynn-dusk", map: Some(MAP_AZEROTH),
- eye: GFX_ELWYNN_EYE, look: GFX_ELWYNN_LOOK, minute: 1170, ui: None,
+ name: "elwynn-dusk",
+ map: Some(MAP_AZEROTH),
+ eye: GFX_ELWYNN_EYE,
+ look: GFX_ELWYNN_LOOK,
+ minute: 1170,
+ ui: None,
},
Scenario {
- name: "elwynn-night", map: Some(MAP_AZEROTH),
- eye: GFX_ELWYNN_EYE, look: GFX_ELWYNN_LOOK, minute: 0, ui: None,
+ name: "elwynn-night",
+ map: Some(MAP_AZEROTH),
+ eye: GFX_ELWYNN_EYE,
+ look: GFX_ELWYNN_LOOK,
+ minute: 0,
+ ui: None,
},
// The same Elwynn view for rain. Capture has no weather field: run it with
// `WOW_WEATHER=1,0.8` (kind 1 rain, grade 0.8; `weather::parse_env_script`).
Scenario {
- name: "elwynn-rain", map: Some(MAP_AZEROTH),
- eye: GFX_ELWYNN_EYE, look: GFX_ELWYNN_LOOK, minute: 720, ui: None,
+ name: "elwynn-rain",
+ map: Some(MAP_AZEROTH),
+ eye: GFX_ELWYNN_EYE,
+ look: GFX_ELWYNN_LOOK,
+ minute: 720,
+ ui: None,
},
// Westfall: a farmstead, fields and the dry palette.
Scenario {
- name: "westfall-farm", map: Some(MAP_AZEROTH),
- eye: [-10080.0, 1000.0, 58.0], look: [-10140.0, 1070.0, 38.0], minute: 720, ui: None,
+ name: "westfall-farm",
+ map: Some(MAP_AZEROTH),
+ eye: [-10080.0, 1000.0, 58.0],
+ look: [-10140.0, 1070.0, 38.0],
+ minute: 720,
+ ui: None,
},
// Redridge: Lake Everstill from above Lakeshire, a wider frame than `water-lake`.
Scenario {
- name: "redridge-lake", map: Some(MAP_AZEROTH),
- eye: [-9350.0, -2340.0, 82.0], look: [-9500.0, -2650.0, 50.0], minute: 720, ui: None,
+ name: "redridge-lake",
+ map: Some(MAP_AZEROTH),
+ eye: [-9350.0, -2340.0, 82.0],
+ look: [-9500.0, -2650.0, 50.0],
+ minute: 720,
+ ui: None,
},
// Duskwood: the road into Darkshire, the zone's short dark fog.
Scenario {
- name: "duskwood-road", map: Some(MAP_AZEROTH),
- eye: [-10560.0, -1000.0, 50.0], look: [-10570.0, -1200.0, 32.0], minute: 720, ui: None,
+ name: "duskwood-road",
+ map: Some(MAP_AZEROTH),
+ eye: [-10560.0, -1000.0, 50.0],
+ look: [-10570.0, -1200.0, 32.0],
+ minute: 720,
+ ui: None,
},
// Burning Steppes: the ash plain and the sky a skybox would replace.
Scenario {
- name: "burning-steppes", map: Some(MAP_AZEROTH),
- eye: [-7700.0, -2100.0, 200.0], look: [-7900.0, -1700.0, 150.0], minute: 720, ui: None,
+ name: "burning-steppes",
+ map: Some(MAP_AZEROTH),
+ eye: [-7700.0, -2100.0, 200.0],
+ look: [-7900.0, -1700.0, 150.0],
+ minute: 720,
+ ui: None,
},
// Blasted Lands: the Dark Portal from the north.
Scenario {
- name: "blasted-lands-portal", map: Some(MAP_AZEROTH),
- eye: [-11700.0, -3200.0, 20.0], look: [-11900.0, -3208.0, 0.0], minute: 720, ui: None,
+ name: "blasted-lands-portal",
+ map: Some(MAP_AZEROTH),
+ eye: [-11700.0, -3200.0, 20.0],
+ look: [-11900.0, -3208.0, 0.0],
+ minute: 720,
+ ui: None,
},
// Mount Hyjal (Kalimdor), inside Light.dbc sphere 270.
Scenario {
- name: "hyjal-mount", map: Some(MAP_KALIMDOR),
- eye: GFX_HYJAL_EYE, look: GFX_HYJAL_LOOK, minute: 720, ui: None,
+ name: "hyjal-mount",
+ map: Some(MAP_KALIMDOR),
+ eye: GFX_HYJAL_EYE,
+ look: GFX_HYJAL_LOOK,
+ minute: 720,
+ ui: None,
},
// Stormwind: the Cathedral of Light's nave (interior WMO lighting).
Scenario {
- name: "stormwind-cathedral-interior", map: Some(MAP_AZEROTH),
- eye: GFX_CATHEDRAL_EYE, look: GFX_CATHEDRAL_LOOK, minute: 720, ui: None,
+ name: "stormwind-cathedral-interior",
+ map: Some(MAP_AZEROTH),
+ eye: GFX_CATHEDRAL_EYE,
+ look: GFX_CATHEDRAL_LOOK,
+ minute: 720,
+ ui: None,
},
// Ironforge: the Great Forge (lava, fire light, a huge interior).
Scenario {
- name: "ironforge-forge", map: Some(MAP_AZEROTH),
- eye: GFX_FORGE_EYE, look: GFX_FORGE_LOOK, minute: 720, ui: None,
+ name: "ironforge-forge",
+ map: Some(MAP_AZEROTH),
+ eye: GFX_FORGE_EYE,
+ look: GFX_FORGE_LOOK,
+ minute: 720,
+ ui: None,
},
// MONKEY (sky): the sky lane's fixtures, from 80 yd above Northshire (clear of the canopy).
// Dusk faces the low sun (az 45°, elev ≈5° at 20:00); night faces away from the moon at 01:00
@@ -1342,38 +1441,68 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
// `WOW_WEATHER=1,0.8 WOW_WETNESS=1 WOW_WET_T=3`, and A/B with `WOW_RAIN_SURFACES=0|1`.
// Goldshire's square: the road, the grass, the inn's roofs and walls.
Scenario {
- name: "wet-goldshire", map: Some(MAP_AZEROTH),
- eye: [-9460.0, 70.0, 59.5], look: [-9452.0, 30.0, 55.0], minute: 720, ui: None,
+ name: "wet-goldshire",
+ map: Some(MAP_AZEROTH),
+ eye: [-9460.0, 70.0, 59.5],
+ look: [-9452.0, 30.0, 55.0],
+ minute: 720,
+ ui: None,
},
// The Elwynn river bank, close enough (< 25 yd) for the rain rings.
Scenario {
- name: "wet-river", map: Some(MAP_AZEROTH),
- eye: [-9514.0, -330.0, 64.0], look: [-9500.0, -352.0, 61.4], minute: 720, ui: None,
+ name: "wet-river",
+ map: Some(MAP_AZEROTH),
+ eye: [-9514.0, -330.0, 64.0],
+ look: [-9500.0, -352.0, 61.4],
+ minute: 720,
+ ui: None,
},
// A Stormwind Trade District street: exterior WMO paving, walls, eaves.
Scenario {
- name: "wet-stormwind", map: Some(MAP_AZEROTH),
- eye: [-8833.38, 628.63, 98.5], look: [-8815.0, 662.0, 93.5], minute: 720, ui: None,
+ name: "wet-stormwind",
+ map: Some(MAP_AZEROTH),
+ eye: [-8833.38, 628.63, 98.5],
+ look: [-8815.0, 662.0, 93.5],
+ minute: 720,
+ ui: None,
},
// MONKEY (fix-wet): the wet-stormwind framing at midnight: lit windows must stay lit in rain.
Scenario {
- name: "wet-stormwind-night", map: Some(MAP_AZEROTH),
- eye: [-8833.38, 628.63, 98.5], look: [-8815.0, 662.0, 93.5], minute: 0, ui: None,
+ name: "wet-stormwind-night",
+ map: Some(MAP_AZEROTH),
+ eye: [-8833.38, 628.63, 98.5],
+ look: [-8815.0, 662.0, 93.5],
+ minute: 0,
+ ui: None,
},
// MONKEY (ao): contact-shadow subjects. Goldshire's street (props against walls, eaves),
// an Elwynn forest floor (trunks, bushes, grass cutouts) and a close unit on open ground.
Scenario {
- name: "ao-goldshire", map: Some(MAP_AZEROTH),
- eye: [-9430.0, 50.0, 61.0], look: [-9462.0, 30.0, 57.5], minute: 720, ui: None,
+ name: "ao-goldshire",
+ map: Some(MAP_AZEROTH),
+ eye: [-9430.0, 50.0, 61.0],
+ look: [-9462.0, 30.0, 57.5],
+ minute: 720,
+ ui: None,
},
Scenario {
- name: "ao-forest", map: Some(MAP_AZEROTH),
- eye: [-9560.0, 60.0, 62.0], look: [-9600.0, 90.0, 58.0], minute: 720, ui: None,
+ name: "ao-forest",
+ map: Some(MAP_AZEROTH),
+ eye: [-9560.0, 60.0, 62.0],
+ look: [-9600.0, 90.0, 58.0],
+ minute: 720,
+ ui: None,
},
Scenario {
- name: "ao-character", map: Some(MAP_AZEROTH),
- eye: [-9495.2, 60.8, 59.0], look: [-9500.00, 56.00, 56.9], minute: 720,
- ui: Some(UiFixture::Subject { kind: SubjectKind::Creature, at: SUBJECT_SUN }),
+ name: "ao-character",
+ map: Some(MAP_AZEROTH),
+ eye: [-9495.2, 60.8, 59.0],
+ look: [-9500.00, 56.00, 56.9],
+ minute: 720,
+ ui: Some(UiFixture::Subject {
+ kind: SubjectKind::Creature,
+ at: SUBJECT_SUN,
+ }),
},
// MONKEY (lampfog): lampFog 0/2 A/B set. Kept at the END so parallel lane tables merge cleanly.
// Goldshire square: the Lion's Pride fixtures and outdoor lamps share one night frame.
diff --git a/crates/benilla-app/src/character_shadow.rs b/crates/benilla-app/src/character_shadow.rs
index cc4cf4dd..cdb5452f 100644
--- a/crates/benilla-app/src/character_shadow.rs
+++ b/crates/benilla-app/src/character_shadow.rs
@@ -6,8 +6,8 @@
//! the per-frame facts the rig publishes ([`ShadowFrame`]). It does not know [`super::world_shadow`]
//! exists — remove either lane and the other is untouched.
-use bevy::pbr::MeshMaterial3d;
use bevy::ecs::entity::EntityHashSet;
+use bevy::pbr::MeshMaterial3d;
use bevy::prelude::*;
use benilla_assets::materials::WowModelMaterial;
@@ -126,7 +126,10 @@ fn collect_character_shadows(
// whose idle animation is breathing has an unchanged `GlobalTransform` and changed VERTICES.
// Gating on the transform would freeze exactly the poses this lane exists to draw. The rate cap
// is the honest saving, and it is bounded work rather than a guess about content.
- if !lane.rate.due(time.elapsed_secs(), video.character_shadow_rate) {
+ if !lane
+ .rate
+ .due(time.elapsed_secs(), video.character_shadow_rate)
+ {
return;
}
if let Some(handle) = lane.mesh.clone() {
diff --git a/crates/benilla-app/src/cvars.rs b/crates/benilla-app/src/cvars.rs
index e02f739b..c003d01e 100644
--- a/crates/benilla-app/src/cvars.rs
+++ b/crates/benilla-app/src/cvars.rs
@@ -2684,15 +2684,33 @@ mod tests {
assert_eq!(cvars.get("shadowDistance"), Some("120"));
assert_eq!(cvars.get("interiorShadowSoft"), Some("2.5"));
// MONKEY (volumetric fog): Off must remove atmosphere as well as enhanced water.
- for member in ["fireLightGain", "waterQuality", "volumetricFog", "lavaLightGain", "nightGain", "interiorGain"] {
+ for member in [
+ "fireLightGain",
+ "waterQuality",
+ "volumetricFog",
+ "lavaLightGain",
+ "nightGain",
+ "interiorGain",
+ ] {
let expected = if *name == "Off" {
- if matches!(member, "fireLightGain" | "waterQuality" | "volumetricFog" | "lavaLightGain") { "0" } else { "1.0" }
- } else if *name == "Ultra" && matches!(member, "waterQuality" | "volumetricFog") {
+ if matches!(
+ member,
+ "fireLightGain" | "waterQuality" | "volumetricFog" | "lavaLightGain"
+ ) {
+ "0"
+ } else {
+ "1.0"
+ }
+ } else if *name == "Ultra" && matches!(member, "waterQuality" | "volumetricFog")
+ {
"2" // MONKEY (presets): Ultra's two maxima over the defaults.
} else {
cvars.default_of(member).unwrap()
};
- assert!(same_value(cvars.get(member).unwrap(), expected), "{name}/{member}");
+ assert!(
+ same_value(cvars.get(member).unwrap(), expected),
+ "{name}/{member}"
+ );
}
}
}
@@ -3010,7 +3028,11 @@ mod tests {
assert!(cvars.reapply_saved_presets() > 0);
assert_eq!(cvars.get("shadowMapSize"), Some("4096"));
assert_eq!(cvars.get("interiorShadowCasters"), Some("16"));
- assert_eq!(cvars.get("volumetricFog"), Some("1"), "a saved member stays");
+ assert_eq!(
+ cvars.get("volumetricFog"),
+ Some("1"),
+ "a saved member stays"
+ );
// No Graphics rung saved: the graphics rows keep their defaults.
assert_eq!(
cvars.get("ambientOcclusion"),
@@ -3154,20 +3176,25 @@ mod tests {
for seeded in [false, true] {
for flush_preset_first in [false, true] {
let mut app = cvar_app();
- app.world_mut().non_send_resource_mut::()
+ app.world_mut()
+ .non_send_resource_mut::()
.register_cvars(registered_pairs());
// Run the production Update schedule without Startup's on-disk config load.
if seeded {
app.world_mut().run_schedule(Update);
}
- app.world_mut().non_send_resource_mut::()
- .run("SetCVar('lightingQuality', 'Low')").unwrap();
+ app.world_mut()
+ .non_send_resource_mut::()
+ .run("SetCVar('lightingQuality', 'Low')")
+ .unwrap();
if flush_preset_first {
app.world_mut().run_schedule(Update);
assert_eq!(res::(&app).shadow_map_size, 1024);
}
- app.world_mut().non_send_resource_mut::()
- .run("SetCVar('shadowMapSize', '4096')").unwrap();
+ app.world_mut()
+ .non_send_resource_mut::()
+ .run("SetCVar('shadowMapSize', '4096')")
+ .unwrap();
for _ in 0..3 {
app.world_mut().run_schedule(Update);
let cvars = res::(&app);
@@ -3176,7 +3203,10 @@ mod tests {
assert_eq!(res::(&app).shadow_map_size, 4096);
let script = app.world().non_send_resource::();
assert_eq!(script.cvar("shadowMapSize").as_deref(), Some("4096"));
- assert_eq!(script.cvar("lightingQuality").as_deref(), Some(LIGHTING_CUSTOM));
+ assert_eq!(
+ script.cvar("lightingQuality").as_deref(),
+ Some(LIGHTING_CUSTOM)
+ );
}
}
}
@@ -3199,7 +3229,12 @@ mod tests {
#[test]
fn water_and_lava_observers_clamp_and_water_high_is_opt_in() {
let mut app = cvar_app();
- for (value, water, lava) in [("-1", 0, 0.0), ("1.75", 1, 1.75), ("2", 2, 2.0), ("9", 2, 4.0)] {
+ for (value, water, lava) in [
+ ("-1", 0, 0.0),
+ ("1.75", 1, 1.75),
+ ("2", 2, 2.0),
+ ("9", 2, 4.0),
+ ] {
apply(&mut app, "waterQuality", value);
apply(&mut app, "lavaLightGain", value);
assert_eq!(res::(&app).water_quality, water);
@@ -3572,7 +3607,11 @@ mod tests {
let player: Vec<&String> = back
.cvars
.keys()
- .filter(|k| !GRAPHICS_PRESETS.iter().any(|(g, _)| g.eq_ignore_ascii_case(k)))
+ .filter(|k| {
+ !GRAPHICS_PRESETS
+ .iter()
+ .any(|(g, _)| g.eq_ignore_ascii_case(k))
+ })
.collect();
assert_eq!(player.len(), 1, "a diff, not a dump: {text}");
assert!(!text.contains("graphicsQuality"), "{text}");
@@ -4176,7 +4215,10 @@ 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 _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"));
@@ -4313,7 +4355,10 @@ mod tests {
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 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) => {
@@ -4324,7 +4369,10 @@ mod tests {
values[col]
)
});
- assert!(same_value(value, reference), "{k}: declared reference is stale");
+ assert!(
+ same_value(value, reference),
+ "{k}: declared reference is stale"
+ );
assert!(!why.trim().is_empty(), "{k}: a deviation owes a reason");
}
_ => assert!(
diff --git a/crates/benilla-app/src/debug_panel/lighting_controls.rs b/crates/benilla-app/src/debug_panel/lighting_controls.rs
index 12190dc3..44ac98a3 100644
--- a/crates/benilla-app/src/debug_panel/lighting_controls.rs
+++ b/crates/benilla-app/src/debug_panel/lighting_controls.rs
@@ -10,7 +10,9 @@ use benilla_ui::script::UiScript;
use bevy_egui::egui;
use crate::cvars::REGISTERED;
-use crate::shadow_core::{CASTER_REACH_RANGE, MAX_SHADOW_RATE, SHADOW_DISTANCE_RANGE, SHADOW_MAP_SIZES};
+use crate::shadow_core::{
+ CASTER_REACH_RANGE, MAX_SHADOW_RATE, SHADOW_DISTANCE_RANGE, SHADOW_MAP_SIZES,
+};
use crate::video::VideoConfig;
enum Widget {
@@ -34,10 +36,20 @@ struct Knob {
// the descriptor read-only without inventing a parallel configuration type.
macro_rules! knob {
($name:literal, $field:ident, $widget:expr, $help:literal) => {
- Knob { name: $name, read: |v| v.$field as f32, widget: $widget, help: $help }
+ Knob {
+ name: $name,
+ read: |v| v.$field as f32,
+ widget: $widget,
+ help: $help,
+ }
};
(flag $name:literal, $field:ident, $help:literal) => {
- Knob { name: $name, read: |v| v.$field as u32 as f32, widget: Widget::Toggle, help: $help }
+ Knob {
+ name: $name,
+ read: |v| v.$field as u32 as f32,
+ widget: Widget::Toggle,
+ help: $help,
+ }
};
}
@@ -46,67 +58,213 @@ use Widget::{Choice, Integer, Slider};
// This developer-only panel intentionally keeps its labels and help text in English.
const INTERIOR: &[Knob] = &[
knob!(flag "interiorLight", interior_light, "Fixture-lit interiors; off restores the baked path."),
- knob!("interiorGain", interior_gain, Slider(0.2..=1.5), "Overall room input brightness."),
- knob!("interiorAmbient", interior_ambient, Slider(0.0..=1.0), "Base room ambient."),
- knob!("interiorFill", interior_fill, Slider(0.0..=2.0), "Per-fixture bounce gain."),
+ knob!(
+ "interiorGain",
+ interior_gain,
+ Slider(0.2..=1.5),
+ "Overall room input brightness."
+ ),
+ knob!(
+ "interiorAmbient",
+ interior_ambient,
+ Slider(0.0..=1.0),
+ "Base room ambient."
+ ),
+ knob!(
+ "interiorFill",
+ interior_fill,
+ Slider(0.0..=2.0),
+ "Per-fixture bounce gain."
+ ),
// MONKEY (bake floor): index 4, immediately after fill, because the three presets below take
// `INTERIOR[1..5]` as one group — this dial belongs with gain/ambient/fill, not with the
// exposure and reach knobs the presets deliberately leave alone.
- knob!("interiorBakeFloor", interior_bake_floor, Slider(0.0..=1.0), "Share of a room's baked light kept where no fixture reaches; 0 = fixtures only."),
- knob!("interiorExposure", interior_exposure, Slider(0.25..=8.0), "Light budget before soft rolloff."),
- knob!("interiorAttenScale", interior_atten_scale, Slider(0.0..=8.0), "Pool radius scale; 0 disables the authored window."),
- knob!("interiorDaylight", interior_daylight, Slider(0.0..=1.0), "Sun-driven day floor in enclosed rooms."),
+ knob!(
+ "interiorBakeFloor",
+ interior_bake_floor,
+ Slider(0.0..=1.0),
+ "Share of a room's baked light kept where no fixture reaches; 0 = fixtures only."
+ ),
+ knob!(
+ "interiorExposure",
+ interior_exposure,
+ Slider(0.25..=8.0),
+ "Light budget before soft rolloff."
+ ),
+ knob!(
+ "interiorAttenScale",
+ interior_atten_scale,
+ Slider(0.0..=8.0),
+ "Pool radius scale; 0 disables the authored window."
+ ),
+ knob!(
+ "interiorDaylight",
+ interior_daylight,
+ Slider(0.0..=1.0),
+ "Sun-driven day floor in enclosed rooms."
+ ),
knob!(flag "interiorRoomGate", interior_room_gate, "Restrict fixtures to the rooms they claim."),
];
const GROUPS: &[(&str, &[Knob])] = &[
- ("Water", &[
- knob!("waterQuality", water_quality, Choice(&[0, 1, 2]), "0 Classic, 1 Enhanced, 2 High; High adds mirror reflections and costs the most."),
- ]),
+ (
+ "Water",
+ &[knob!(
+ "waterQuality",
+ water_quality,
+ Choice(&[0, 1, 2]),
+ "0 Classic, 1 Enhanced, 2 High; High adds mirror reflections and costs the most."
+ )],
+ ),
("Interior light", INTERIOR),
- ("Fire", &[
- knob!("fireLightGain", fire_light_gain, Slider(0.0..=4.0), "Brightness of lights synthesized from flame emitters."),
- // MONKEY (spellLightGain): in the "Fire" group because a spell light IS one of the invented
- // sources this group tunes — it is just the one whose brightness is a combat setting rather
- // than a scenery one, which is exactly why it needs its own dial and not the one above it.
- knob!("spellLightGain", spell_light_gain, Slider(0.0..=4.0), "Brightness of spell, missile and impact lights."),
- knob!("lavaLightGain", lava_light_gain, Slider(0.0..=4.0), "How strongly lava lights its surroundings; 0 turns the glow off."),
- knob!("fireFlicker", fire_flicker, Slider(0.0..=2.0), "0 steady, 1 default, 2 pronounced."),
- ]),
- ("Torch shadows", &[
- knob!(flag "interiorShadows", interior_shadows, "Interior fixture shadows; requires interiorLight."),
- knob!(flag "exteriorShadows", exterior_shadows, "Outdoor fire shadows at night."),
- knob!("interiorShadowCasters", interior_shadow_casters, Integer(1.0..=16.0), "Resident fixture shadow maps."),
- knob!("interiorShadowDynamic", interior_shadow_dynamic, Integer(1.0..=crate::torch_shadow::MAX_TORCH_DYNAMIC as f32), "Nearest fixtures with moving entity shadows."),
- knob!("interiorShadowSoft", interior_shadow_soft, Slider(0.5..=3.0), "Shadow edge softness at a contact; the penumbra grows from it."),
- knob!("torchShadowStrength", torch_shadow_strength, Slider(0.0..=1.0), "Shadow darkness: how much direct light a shadow removes. 1 = black."),
- knob!("interiorShadowEntityRate", interior_shadow_entity_rate, Integer(0.0..=240.0), "Moving caster regather Hz; 0 = every frame."),
- ]),
- ("Sun shadows", &[
- knob!(flag "characterShadows", character_shadows, "Realtime character silhouettes instead of blobs."),
- knob!(flag "worldShadows", world_shadows, "Realtime static-world shadows instead of baked terrain shadows."),
- knob!("shadowDistance", shadow_distance, Slider(SHADOW_DISTANCE_RANGE), "Shadow distance in yards."),
- knob!("shadowMapSize", shadow_map_size, Choice(&SHADOW_MAP_SIZES), "Map edge in texels; larger maps cost more GPU time and memory."),
- knob!("shadowFilter", shadow_filter, Widget::Toggle, "0 hardware 2x2, 1 Gaussian PCF."),
- knob!("characterShadowRate", character_shadow_rate, Integer(0.0..=MAX_SHADOW_RATE as f32), "Character shadow update Hz; 0 = every frame."),
- knob!("worldShadowRate", world_shadow_rate, Integer(0.0..=MAX_SHADOW_RATE as f32), "World shadow update Hz; 0 = every frame."),
- knob!("shadowCasterReach", shadow_caster_reach, Slider(CASTER_REACH_RANGE), "Caster collection reach multiplier."),
- ]),
- ("Night", &[
- knob!("nightGain", night_gain, Slider(0.2..=1.5), "Exterior night brightness; inert by day."),
- // MONKEY (moon shadows): in "Night" rather than beside the sun dials, because that is
- // where both its cost and its taste question live — it is inert by day, and what it buys
- // is "the world is not flat after dark" for a second shadowed directional term.
- knob!("moonShadowStrength", moon_shadow_strength, Slider(0.0..=1.0), "How dark a moon-shadowed fragment gets at night; 0 = no moon shadow."),
- ]),
- ("Debug", &[
- knob!("interiorDebug", interior_debug, Choice(&[0, 1, 2, 3, 4]), "0 off, 1 classification, 2 shadow, 3 caster count, 4 WMO lane map."),
- ]),
+ (
+ "Fire",
+ &[
+ knob!(
+ "fireLightGain",
+ fire_light_gain,
+ Slider(0.0..=4.0),
+ "Brightness of lights synthesized from flame emitters."
+ ),
+ // MONKEY (spellLightGain): in the "Fire" group because a spell light IS one of the invented
+ // sources this group tunes — it is just the one whose brightness is a combat setting rather
+ // than a scenery one, which is exactly why it needs its own dial and not the one above it.
+ knob!(
+ "spellLightGain",
+ spell_light_gain,
+ Slider(0.0..=4.0),
+ "Brightness of spell, missile and impact lights."
+ ),
+ knob!(
+ "lavaLightGain",
+ lava_light_gain,
+ Slider(0.0..=4.0),
+ "How strongly lava lights its surroundings; 0 turns the glow off."
+ ),
+ knob!(
+ "fireFlicker",
+ fire_flicker,
+ Slider(0.0..=2.0),
+ "0 steady, 1 default, 2 pronounced."
+ ),
+ ],
+ ),
+ (
+ "Torch shadows",
+ &[
+ knob!(flag "interiorShadows", interior_shadows, "Interior fixture shadows; requires interiorLight."),
+ knob!(flag "exteriorShadows", exterior_shadows, "Outdoor fire shadows at night."),
+ knob!(
+ "interiorShadowCasters",
+ interior_shadow_casters,
+ Integer(1.0..=16.0),
+ "Resident fixture shadow maps."
+ ),
+ knob!(
+ "interiorShadowDynamic",
+ interior_shadow_dynamic,
+ Integer(1.0..=crate::torch_shadow::MAX_TORCH_DYNAMIC as f32),
+ "Nearest fixtures with moving entity shadows."
+ ),
+ knob!(
+ "interiorShadowSoft",
+ interior_shadow_soft,
+ Slider(0.5..=3.0),
+ "Shadow edge softness at a contact; the penumbra grows from it."
+ ),
+ knob!(
+ "torchShadowStrength",
+ torch_shadow_strength,
+ Slider(0.0..=1.0),
+ "Shadow darkness: how much direct light a shadow removes. 1 = black."
+ ),
+ knob!(
+ "interiorShadowEntityRate",
+ interior_shadow_entity_rate,
+ Integer(0.0..=240.0),
+ "Moving caster regather Hz; 0 = every frame."
+ ),
+ ],
+ ),
+ (
+ "Sun shadows",
+ &[
+ knob!(flag "characterShadows", character_shadows, "Realtime character silhouettes instead of blobs."),
+ knob!(flag "worldShadows", world_shadows, "Realtime static-world shadows instead of baked terrain shadows."),
+ knob!(
+ "shadowDistance",
+ shadow_distance,
+ Slider(SHADOW_DISTANCE_RANGE),
+ "Shadow distance in yards."
+ ),
+ knob!(
+ "shadowMapSize",
+ shadow_map_size,
+ Choice(&SHADOW_MAP_SIZES),
+ "Map edge in texels; larger maps cost more GPU time and memory."
+ ),
+ knob!(
+ "shadowFilter",
+ shadow_filter,
+ Widget::Toggle,
+ "0 hardware 2x2, 1 Gaussian PCF."
+ ),
+ knob!(
+ "characterShadowRate",
+ character_shadow_rate,
+ Integer(0.0..=MAX_SHADOW_RATE as f32),
+ "Character shadow update Hz; 0 = every frame."
+ ),
+ knob!(
+ "worldShadowRate",
+ world_shadow_rate,
+ Integer(0.0..=MAX_SHADOW_RATE as f32),
+ "World shadow update Hz; 0 = every frame."
+ ),
+ knob!(
+ "shadowCasterReach",
+ shadow_caster_reach,
+ Slider(CASTER_REACH_RANGE),
+ "Caster collection reach multiplier."
+ ),
+ ],
+ ),
+ (
+ "Night",
+ &[
+ knob!(
+ "nightGain",
+ night_gain,
+ Slider(0.2..=1.5),
+ "Exterior night brightness; inert by day."
+ ),
+ // MONKEY (moon shadows): in "Night" rather than beside the sun dials, because that is
+ // where both its cost and its taste question live — it is inert by day, and what it buys
+ // is "the world is not flat after dark" for a second shadowed directional term.
+ knob!(
+ "moonShadowStrength",
+ moon_shadow_strength,
+ Slider(0.0..=1.0),
+ "How dark a moon-shadowed fragment gets at night; 0 = no moon shadow."
+ ),
+ ],
+ ),
+ (
+ "Debug",
+ &[knob!(
+ "interiorDebug",
+ interior_debug,
+ Choice(&[0, 1, 2, 3, 4]),
+ "0 off, 1 classification, 2 shadow, 3 caster count, 4 WMO lane map."
+ )],
+ ),
];
fn reset(script: &mut UiScript, knobs: &[Knob]) {
for knob in knobs {
- let registered = REGISTERED.iter().find(|r| r.name == knob.name)
+ let registered = REGISTERED
+ .iter()
+ .find(|r| r.name == knob.name)
.expect("lighting panel knob must be registered");
script.set_cvar_engine(registered.name, registered.default);
}
@@ -117,35 +275,40 @@ fn row(ui: &mut egui::Ui, video: &VideoConfig, script: &mut UiScript, knob: &Kno
// MONKEY (lighting debug panel): stack the label over numeric controls so even the longest
// CVar fits the existing 280 px panel. Sliders retain their editable current-value readout;
// integer knobs cannot queue fractional counts/rates, and flags explicitly show their 0/1.
- let changed = ui.push_id(knob.name, |ui| match &knob.widget {
- Widget::Toggle => {
- let mut enabled = value != 0.0;
- let response = ui.checkbox(&mut enabled, format!("{} {}", knob.name, value as u32))
- .on_hover_text(knob.help);
- value = enabled as u32 as f32;
- response.changed()
- }
- Slider(range) | Integer(range) => {
- ui.label(knob.name).on_hover_text(knob.help);
- let mut slider = egui::Slider::new(&mut value, range.clone());
- if matches!(knob.widget, Integer(_)) {
- slider = slider.integer();
+ let changed = ui
+ .push_id(knob.name, |ui| match &knob.widget {
+ Widget::Toggle => {
+ let mut enabled = value != 0.0;
+ let response = ui
+ .checkbox(&mut enabled, format!("{} {}", knob.name, value as u32))
+ .on_hover_text(knob.help);
+ value = enabled as u32 as f32;
+ response.changed()
}
- ui.add(slider).on_hover_text(knob.help).changed()
- }
- Choice(options) => {
- ui.label(knob.name).on_hover_text(knob.help);
- let mut changed = false;
- egui::ComboBox::from_id_salt("value")
- .selected_text((value as u32).to_string())
- .show_ui(ui, |ui| {
- for &option in *options {
- changed |= ui.selectable_value(&mut value, option as f32, option.to_string()).changed();
- }
- });
- changed
- }
- }).inner;
+ Slider(range) | Integer(range) => {
+ ui.label(knob.name).on_hover_text(knob.help);
+ let mut slider = egui::Slider::new(&mut value, range.clone());
+ if matches!(knob.widget, Integer(_)) {
+ slider = slider.integer();
+ }
+ ui.add(slider).on_hover_text(knob.help).changed()
+ }
+ Choice(options) => {
+ ui.label(knob.name).on_hover_text(knob.help);
+ let mut changed = false;
+ egui::ComboBox::from_id_salt("value")
+ .selected_text((value as u32).to_string())
+ .show_ui(ui, |ui| {
+ for &option in *options {
+ changed |= ui
+ .selectable_value(&mut value, option as f32, option.to_string())
+ .changed();
+ }
+ });
+ changed
+ }
+ })
+ .inner;
if changed {
script.set_cvar_engine(knob.name, &value.to_string());
}
@@ -162,8 +325,9 @@ pub(super) fn section(ui: &mut egui::Ui, video: &VideoConfig, script: Option<&mu
// off the VM mirror rather than derived locally, because the registry's own per-frame
// derivation is the one authority on it and a second derivation would be a second opinion.
ui.horizontal(|ui| {
- ui.label("Lighting quality")
- .on_hover_text("Writes the member rows below; reads back Custom when they match no rung.");
+ ui.label("Lighting quality").on_hover_text(
+ "Writes the member rows below; reads back Custom when they match no rung.",
+ );
let current = script.cvar("lightingQuality").unwrap_or_default();
for (name, _) in crate::cvars::LIGHTING_PRESETS {
if ui
@@ -191,7 +355,13 @@ pub(super) fn section(ui: &mut egui::Ui, video: &VideoConfig, script: Option<&mu
("Default", None),
("Bright", Some(["1.0", "0.04", "0.16", "0.2"])),
] {
- if ui.small_button(label).on_hover_text("Only gain, ambient, fill and bake floor; other settings stay as they are.").clicked() {
+ if ui
+ .small_button(label)
+ .on_hover_text(
+ "Only gain, ambient, fill and bake floor; other settings stay as they are.",
+ )
+ .clicked()
+ {
if let Some(values) = values {
for (knob, value) in INTERIOR[1..5].iter().zip(values) {
script.set_cvar_engine(knob.name, value);
@@ -202,7 +372,11 @@ pub(super) fn section(ui: &mut egui::Ui, video: &VideoConfig, script: Option<&mu
}
}
});
- if ui.small_button("Reset all").on_hover_text("Restore registry defaults for every group in Lighting & shadows.").clicked() {
+ if ui
+ .small_button("Reset all")
+ .on_hover_text("Restore registry defaults for every group in Lighting & shadows.")
+ .clicked()
+ {
for (_, knobs) in GROUPS {
reset(script, knobs);
}
diff --git a/crates/benilla-app/src/dynamic_interior.rs b/crates/benilla-app/src/dynamic_interior.rs
index 5b0a1e8b..08d3b8d4 100644
--- a/crates/benilla-app/src/dynamic_interior.rs
+++ b/crates/benilla-app/src/dynamic_interior.rs
@@ -26,7 +26,8 @@
//! Water quality and lava glow use the same guarded bridge into their renderer resources.
use benilla_assets::WaterQuality;
use benilla_world::lighting::{
- DynamicInteriors, EmissiveTier, FireLightGain, LavaLightGain, MoonShadowStrength, SpellLightGain,
+ DynamicInteriors, EmissiveTier, FireLightGain, LavaLightGain, MoonShadowStrength,
+ SpellLightGain,
};
use bevy::prelude::*;
@@ -166,8 +167,14 @@ mod tests {
.init_resource::()
.init_resource::()
.add_plugins(DynamicInteriorPlugin);
- assert_eq!(app.world().resource::().0, VideoConfig::default().water_quality);
- assert_eq!(app.world().resource::().0, VideoConfig::default().lava_light_gain);
+ assert_eq!(
+ app.world().resource::().0,
+ VideoConfig::default().water_quality
+ );
+ assert_eq!(
+ app.world().resource::().0,
+ VideoConfig::default().lava_light_gain
+ );
app.update();
for (water, lava) in [(0, 0.0), (2, 3.25), (1, 1.0)] {
{
diff --git a/crates/benilla-app/src/entities/attach/char_skin.rs b/crates/benilla-app/src/entities/attach/char_skin.rs
index 70aa8a49..9d3439b1 100644
--- a/crates/benilla-app/src/entities/attach/char_skin.rs
+++ b/crates/benilla-app/src/entities/attach/char_skin.rs
@@ -94,11 +94,14 @@ pub(super) fn resolve_char_look(
None
}
-/// The `mpq://` URL of a baked NPC atlas under `Textures\BakedNpcTextures\`.
+/// The `mpq://` URL of a baked NPC atlas: [`benilla_formats::baked_atlas_path`], as a lowercase
+/// forward-slash URL (the reference appends `.blp` to a bake name that lacks it).
fn baked_npc_url(bake_name: &str) -> String {
format!(
- "mpq://textures/bakednpctextures/{}",
- bake_name.replace('\\', "/").to_ascii_lowercase()
+ "mpq://{}",
+ benilla_formats::baked_atlas_path(bake_name)
+ .replace('\\', "/")
+ .to_ascii_lowercase()
)
}
@@ -396,3 +399,27 @@ pub(super) fn build_char_skin_materials(
(body, hair, object, skin_extra)
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn baked_npc_url_appends_the_extension_a_bare_bake_name_lacks() {
+ // A shipped bake name already carries `.blp`.
+ assert_eq!(
+ baked_npc_url("Northwindbailiff.blp"),
+ "mpq://textures/bakednpctextures/northwindbailiff.blp"
+ );
+ // A bare bake name (TurtleWoW stores these) resolves once `.blp` is appended, which also
+ // gives the loader the extension it keys on.
+ assert_eq!(
+ baked_npc_url("Northwindbutcher"),
+ "mpq://textures/bakednpctextures/northwindbutcher.blp"
+ );
+ assert_eq!(
+ baked_npc_url("kultirasrepresentive"),
+ "mpq://textures/bakednpctextures/kultirasrepresentive.blp"
+ );
+ }
+}
diff --git a/crates/benilla-app/src/entities/attach/mod.rs b/crates/benilla-app/src/entities/attach/mod.rs
index 0055355e..b6c4d7e0 100644
--- a/crates/benilla-app/src/entities/attach/mod.rs
+++ b/crates/benilla-app/src/entities/attach/mod.rs
@@ -744,9 +744,13 @@ pub(super) fn attach_entity_visuals(
// stands still, so it stays eligible. `net.kind` is the only place the two are told
// apart: downstream both are just a `PointLight` under a `ChildOf`.
let body_carried = !matches!(net.kind, EntityKind::GameObject);
- super::spawn_carried_lights(&mut commands, model_lights, entity, body_carried, |bone| {
- light_anchors.get(&bone).copied()
- });
+ super::spawn_carried_lights(
+ &mut commands,
+ model_lights,
+ entity,
+ body_carried,
+ |bone| light_anchors.get(&bone).copied(),
+ );
// Ribbon trails, on the same host-bone ride; each despawns with its owner.
{
for rb in dm.map(|d| d.ribbons.as_slice()).unwrap_or_default() {
diff --git a/crates/benilla-app/src/entities/carried_light.rs b/crates/benilla-app/src/entities/carried_light.rs
index e3dc2f47..3f45db68 100644
--- a/crates/benilla-app/src/entities/carried_light.rs
+++ b/crates/benilla-app/src/entities/carried_light.rs
@@ -458,7 +458,11 @@ pub(crate) fn track_carried_light_motion(
time: Res