diff --git a/crates/benilla-app/src/capture/fixtures.rs b/crates/benilla-app/src/capture/fixtures.rs index 6fae9c0d..fa44d69c 100644 --- a/crates/benilla-app/src/capture/fixtures.rs +++ b/crates/benilla-app/src/capture/fixtures.rs @@ -719,10 +719,47 @@ pub(super) fn seed_ui_fixture( player.pos = benilla_assets::coords::wow_to_bevy(scenario.eye); // Alternating explore bits, so about half of each zone's overlays reveal. script.set_world_map_explored(vec![0x5555_5555; 64]); - // The Elwynn zone map: continent 2, zone 10 in the alphabetical zone list. + // Continent 2, zone 10 of the alphabetical zone list, which is Dun Morogh — the + // comment here said Elwynn and the shot has never been Elwynn. if let Err(e) = script.run("ToggleWorldMap(); SetMapZoom(2, 10)") { warn!("capture: ui-worldmap seed failed to open the map: {e}"); } + // With `$WOW_CAPTURE_ADDONS=1` a map addon has taken the window over and draws its own + // art, and the defects worth shooting are the ones that only appear zoomed in — a + // stitched map at rest looks right and comes apart at a zone's own scale. So drive the + // addon's published view: `WOW_CAPTURE_MAP_ZOOM=` (1 is the whole world) and + // `WOW_CAPTURE_MAP_AT=,` (normalized on the stitched world; the default is Dun + // Morogh's centre, which is where grom's report came from). Nothing happens without + // an addon publishing `OpenMapAPI`, so a stock capture is untouched. + if let Ok(zoom) = std::env::var("WOW_CAPTURE_MAP_ZOOM") { + let at = std::env::var("WOW_CAPTURE_MAP_AT").unwrap_or_default(); + let (u, v) = at + .split_once(',') + .and_then(|(u, v)| { + Some((u.trim().parse::().ok()?, v.trim().parse().ok()?)) + }) + .unwrap_or((0.7655, 0.4942)); + let zoom: f32 = zoom.parse().unwrap_or(8.0); + // Asked for on a retry rather than once: the fixture runs the moment the scene + // settles, and a stitched map is not built until the addon has seen the window + // open, so a single call lands before there is anything to drive and answers + // false. The retry stops at the first success, because `Glide` animates and a + // second call would restart the travel every frame. `$WOW_CAPTURE_AGE` is what + // gives the art time to arrive after it. + let chunk = format!( + "local f = CreateFrame(\"Frame\") local n = 0 \ + f:SetScript(\"OnUpdate\", function() \ + n = n + 1 \ + if OpenMapAPI and OpenMapAPI.ShowWorld and OpenMapAPI.ShowWorld({u}, {v}, {zoom}) then \ + OpenMapCaptureView = n f:SetScript(\"OnUpdate\", nil) \ + elseif n > 600 then OpenMapCaptureView = -1 f:SetScript(\"OnUpdate\", nil) end \ + end)" + ); + match script.run(&chunk) { + Ok(()) => info!("capture: map addon view armed for ({u}, {v}) zoom {zoom}"), + Err(e) => warn!("capture: driving the map addon's view: {e}"), + } + } } UiFixture::PartyInvite => { let Some(script) = script else { diff --git a/crates/benilla-app/src/local_state.rs b/crates/benilla-app/src/local_state.rs index 6812b9b5..3fef3d28 100644 --- a/crates/benilla-app/src/local_state.rs +++ b/crates/benilla-app/src/local_state.rs @@ -25,6 +25,13 @@ pub(crate) fn home() -> Option { if std::env::var_os("WOW_CAPTURE").is_some() { return None; // hermetic: captures neither read nor write player state } + home_ignoring_capture() +} + +/// [`home`] without the capture gate, for the one caller that may opt back in: +/// `ui_script::addons::root` under `$WOW_CAPTURE_ADDONS`. Nothing else should use it — the +/// hermetic rule is what makes a baseline comparable between machines. +pub(crate) fn home_ignoring_capture() -> Option { if let Some(over) = std::env::var_os("BENILLA_HOME") { return Some(PathBuf::from(over)); } diff --git a/crates/benilla-app/src/ui_script/addons.rs b/crates/benilla-app/src/ui_script/addons.rs index ca7ea269..bbc5b162 100644 --- a/crates/benilla-app/src/ui_script/addons.rs +++ b/crates/benilla-app/src/ui_script/addons.rs @@ -322,6 +322,19 @@ const ADDONS_PREFIX: &str = "Interface/AddOns/"; /// `Interface\AddOns\`; benilla reads an install and does not live in one, so its addons live in /// its own state folder, and only there. pub(crate) fn root() -> Option { + // `WOW_CAPTURE_ADDONS=1`: put the player's own addons into a capture. A capture is otherwise + // hermetic — `local_state::home` answers `None` under `$WOW_CAPTURE`, so a baseline is the + // shipped interface and nothing else, and `scripts/visual.sh` never sets this. It exists + // because an addon that draws its own art is otherwise unseeable without a server: the addon + // harness has no renderer, and a screen recording from a player has no knobs. With it, + // `WOW_CAPTURE=ui-worldmap WOW_CAPTURE_ADDONS=1` puts a map addon's own drawing in front of + // the real renderer at a chosen zoom (`capture::fixtures`' `WOW_CAPTURE_MAP_ZOOM`). + // + // Saved variables still do not load (that is `local_state::home` too), so an addon runs on + // its shipped defaults, not the player's settings. + if std::env::var("WOW_CAPTURE_ADDONS").as_deref() == Ok("1") { + return root_from(crate::local_state::home_ignoring_capture()); + } root_from(crate::local_state::home()) }