capture: WOW_CAPTURE_ADDONS=1, to put an addon's own drawing in front of the renderer

A map report could be answered from neither harness: `addon_harness` runs the
VM with no renderer, so it can say what a region is and never what it looks
like, and the capture harness renders for real but is hermetic —
`local_state::home` answers None under $WOW_CAPTURE, so no config, no saved
variables and no addons, which is what makes a baseline comparable between
machines. An addon that draws its own art was therefore unseeable without a
server, and the only evidence was a player's screen recording.

`WOW_CAPTURE_ADDONS=1` opts the player's addon folder back in for one run —
`ui_script::addons::root`, the single narrow exception, with saved variables
still out, so an addon runs on its shipped defaults. The world-map fixture will
also ask a stitched-map addon for a view through its published API, retrying
until the map is stitched (`WOW_CAPTURE_MAP_ZOOM`, `WOW_CAPTURE_MAP_AT`).
`scripts/visual.sh` never sets any of it, so no baseline sees an addon.

The first shot taken this way answered the open question: with OpenMapClassic
loaded this client draws the stitched Dun Morogh map correctly, art, neighbours,
labels, lore panel and player dot — so the map report is not the art pipeline.

Also corrects a stale comment: the shot has never been Elwynn; continent 2,
zone 10 is Dun Morogh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-11-02 21:15:38 -07:00
co-authored by Claude Opus 5
parent d39d3ec5cf
commit ba11fc3315
3 changed files with 58 additions and 1 deletions
+38 -1
View File
@@ -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=<zoom>` (1 is the whole world) and
// `WOW_CAPTURE_MAP_AT=<u>,<v>` (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::<f32>().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 {
+7
View File
@@ -25,6 +25,13 @@ pub(crate) fn home() -> Option<PathBuf> {
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<PathBuf> {
if let Some(over) = std::env::var_os("BENILLA_HOME") {
return Some(PathBuf::from(over));
}
@@ -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<PathBuf> {
// `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())
}