the golden sweep gains two hand-framed spots and stops faking green

- Two new golden-capture spots framed by a human eye rather than a lane list:
  the Goldshire inn common room (the interior everyone actually stands in) and
  a Felwood vista. The sweep also captures the world-map field now.
- An honest-tooling fix: a scenario that failed to produce its image used to
  vanish from the diff and the sweep still read green. A missing shot is now
  its own loud failure — a clean sweep means every frame was actually compared.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-07-29 19:51:57 +07:00
co-authored by Claude Fable 5
parent 993adf79b9
commit fa5fa6dbce
5 changed files with 192 additions and 20 deletions
+35 -9
View File
@@ -140,12 +140,22 @@ fn main() -> Result<()> {
}
"diff-dir" => {
let [da, db] = two(rest, "diff-dir")?;
let worst = diff_dir(
let DirDiff { worst, unpaired } = diff_dir(
Path::new(da),
Path::new(db),
opts.out.as_deref(),
opts.amplify,
)?;
// Under `--fail` this is a gate, and a shot that never landed must sink it — see the
// note in `diff_dir` (decision 0743).
if opts.fail.is_some() && !unpaired.is_empty() {
bail!(
"{} unpaired image(s) — a shot is missing from one side, so this comparison is \
incomplete: {}",
unpaired.len(),
unpaired.join(", ")
);
}
if let Some((name, m)) = worst {
if over_fail(&m, opts.fail) {
bail!(
@@ -679,13 +689,16 @@ fn diff_one(a: &Path, b: &Path, out: Option<&Path>, amplify: u32) -> Result<Metr
}
/// Compare every `*.png` present in both dirs (by file name). Returns the worst (highest-MAE) result.
fn diff_dir(
da: &Path,
db: &Path,
out: Option<&Path>,
amplify: u32,
) -> Result<Option<(String, Metrics)>> {
let names: BTreeSet<String> = pngs(da)?.intersection(&pngs(db)?).cloned().collect();
/// What one `diff-dir` run found: the worst pair by MAE, and every image present on only ONE side.
struct DirDiff {
worst: Option<(String, Metrics)>,
/// Files with no counterpart, so not diffed at all — see the note in [`diff_dir`].
unpaired: Vec<String>,
}
fn diff_dir(da: &Path, db: &Path, out: Option<&Path>, amplify: u32) -> Result<DirDiff> {
let (a, b) = (pngs(da)?, pngs(db)?);
let names: BTreeSet<String> = a.intersection(&b).cloned().collect();
if names.is_empty() {
bail!(
"no common *.png files between {} and {}",
@@ -693,6 +706,19 @@ fn diff_dir(
db.display()
);
}
// UNPAIRED shots are ANNOUNCED, never skipped in silence. Pairing on the intersection alone is
// how a sweep reports all-green with a shot missing: on 2026-07-28 one `water-night` capture
// exited 0 without writing its PNG, and `selfcheck` diffed the other eight and passed clean. A
// gate that quietly narrows its own scope is worse than no gate. Reported always; fatal to the
// CALLER when `--fail` is in force, i.e. whenever this is being used as a gate (decision 0743).
let unpaired: Vec<String> = a
.difference(&b)
.map(|n| format!("{}/{n}", da.display()))
.chain(b.difference(&a).map(|n| format!("{}/{n}", db.display())))
.collect();
for u in &unpaired {
eprintln!("UNPAIRED (no counterpart — NOT diffed): {u}");
}
if let Some(out) = out {
std::fs::create_dir_all(out).ok();
}
@@ -708,7 +734,7 @@ fn diff_dir(
if let Some((name, m)) = &worst {
println!("worst: {name} (MAE {:.3})", m.mae);
}
Ok(worst)
Ok(DirDiff { worst, unpaired })
}
fn pngs(dir: &Path) -> Result<BTreeSet<String>> {
+34 -3
View File
@@ -376,7 +376,8 @@ impl Plugin for CapturePlugin {
.init_resource::<FxViewState>();
Scenario {
name: "fxview",
eye: GROUND_EYE, // overridden per frame by the orbit in `pin_scene`
map: scenarios::MAP_AZEROTH, // the fixture spawns over the Northshire slope
eye: GROUND_EYE, // overridden per frame by the orbit in `pin_scene`
look: FXVIEW_POS,
minute: 720,
ui: None,
@@ -418,6 +419,7 @@ impl Plugin for CapturePlugin {
.init_resource::<FxViewState>();
Scenario {
name: "waterfx",
map: scenarios::MAP_AZEROTH, // the synthetic lattice sits over the Northshire slope
eye,
look: center,
minute: 720,
@@ -459,6 +461,12 @@ impl Plugin for CapturePlugin {
let d = 500.0_f32;
Scenario {
name: "vista",
// The arbitrary-viewpoint instrument goes anywhere, so its map is a knob: a
// horizon report from Kalimdor is `WOW_MAP=1` (a `Map.dbc` id).
map: std::env::var("WOW_MAP")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(scenarios::MAP_AZEROTH),
eye,
look: [
eye[0] + d * pitch.cos() * face.cos(),
@@ -487,6 +495,14 @@ impl Plugin for CapturePlugin {
);
std::process::exit(2);
};
// Seed the continent the scenario names, BEFORE `world_map::load_world_map` reads it at
// `Startup` — that is the single place `CurrentMap` is set for a server-less run, and the
// terrain/WDL streamers and per-map lighting all key off it. Raw WoW coords repeat on every
// continent, so a scenario that could not say which map it meant would stream the wrong
// world's tiles and photograph a void (Felwood's tile `33_24` exists in Azeroth, empty).
// Written back for `vista` too, which is where the value came from — a harmless no-op that
// keeps one path for "which map is this run on" (decision 0743).
std::env::set_var("WOW_MAP", scenario.map.to_string());
let out = std::env::var("WOW_CAPTURE_OUT")
.unwrap_or_else(|_| format!("target/visual/{}.png", scenario.name));
if let Some(parent) = Path::new(&out).parent() {
@@ -850,8 +866,23 @@ fn drive_capture(
}
Phase::Done(n) => {
if n + 1 >= EXIT_GRACE_FRAMES {
info!("capture: saved {}, exiting", ctx.out);
exit.write(AppExit::Success);
// Exit on what is ON DISK, not on having reached this phase. The save is async and
// the `Saving` phase gives up after `SAVE_TIMEOUT_FRAMES` so a missed `Capturing`
// marker can't hang the harness — but "gave up" used to exit Success anyway, so a
// capture that wrote nothing reported success and the sweep carried on around the
// hole (2026-07-28: one `water-night` run left no PNG, and `selfcheck` passed on
// the remaining eight). A missing file is a FAILED capture and says so, so
// `scripts/visual.sh`'s `set -e` stops the sweep at it (decision 0743).
if Path::new(&ctx.out).is_file() {
info!("capture: saved {}, exiting", ctx.out);
exit.write(AppExit::Success);
} else {
error!(
"capture: FAILED — no file at {} after the save window; exiting nonzero",
ctx.out
);
exit.write(AppExit::error());
}
Phase::Done(n)
} else {
Phase::Done(n + 1)
+113 -5
View File
@@ -7,6 +7,11 @@
#[derive(Clone, Copy)]
pub(super) struct Scenario {
pub(super) name: &'static str,
/// `Map.dbc` id the eye/look coords belong to. Raw WoW coords repeat on every continent — the
/// Felwood spot's tile (`33_24`) exists in Azeroth too, empty — so a scenario that could not
/// name its map silently photographed the wrong world. The harness seeds
/// [`crate::world_map::CurrentMap`] from this before streaming starts (decision 0743).
pub(super) map: u32,
/// Camera eye, raw WoW coords `(x, y, z)`.
pub(super) eye: [f32; 3],
/// Camera look-at target, raw WoW coords.
@@ -110,11 +115,16 @@ pub(super) const SKY_LOOK: [f32; 3] = [-8740.0, 80.0, 168.0]; // up + out: horiz
// Abbey, one of the few buildings immune to it. Baselines must cover ordinary buildings too.
pub(super) const HOUSE_EYE: [f32; 3] = [-9439.1, 71.2, 68.0];
/// THE golden baseline: three spots FRAMED BY THE DIRECTOR in the live client ([`/shot`], the
/// `Map.dbc` ids the golden spots stand on.
pub(super) const MAP_AZEROTH: u32 = 0;
pub(super) const MAP_KALIMDOR: u32 = 1;
/// THE golden baseline: five spots FRAMED BY THE DIRECTOR in the live client ([`/shot`], the
/// numbers below verbatim from their `~/.benilla/shots.txt`) — the Northshire overlook toward the
/// Abbey, a Stormwind canal view, and Elwynn water — each at noon and at night.
/// Abbey, a Stormwind canal view, Elwynn water, the Lion's Pride Inn common room, and a Felwood
/// hollow — each at noon and at night, except the inn (which the clock barely reaches; see below).
///
/// Six shots of maximal mutual difference. The old set was thirty, and the director judged the
/// Nine shots of maximal mutual difference. The old set was thirty, and the director judged the
/// design bad: low-value repetition (four compass looks at one farmhouse, five frames from one
/// Northshire camera, sixteen UI windows over the same backdrop), each one a real window popping
/// open on their screen while they worked. The law is **a few spots chosen with the director, times
@@ -122,9 +132,17 @@ pub(super) const HOUSE_EYE: [f32; 3] = [-9439.1, 71.2, 68.0];
/// blessed sweep. (Decision 0632; ported from the sibling `substrate` fork's `notes/0025`, where the
/// redesign was made and where it caught a real determinism bug — see `liquid::animate_liquid` — on
/// its first run.)
///
/// The first six were all EXTERIOR, all Azeroth, all Elwynn/Stormwind. The two spots added in
/// decision 0743 are the two things that set of six could not photograph at all: the inside of a
/// building (portal culling, the INT vertex bake, MOCV self-illum, MOLT point pools, props) and a
/// second continent (a different tileset, fog palette, liquid type and horizon). Both were framed
/// by the director after the agent's own framings of the same two subjects were judged bad — which
/// is 0632's rule working exactly as intended.
pub(super) const SCENARIOS: &[Scenario] = &[
Scenario {
name: "overlook-noon",
map: MAP_AZEROTH,
eye: OVERLOOK_EYE,
look: OVERLOOK_LOOK,
minute: 720,
@@ -132,6 +150,7 @@ pub(super) const SCENARIOS: &[Scenario] = &[
},
Scenario {
name: "overlook-night",
map: MAP_AZEROTH,
eye: OVERLOOK_EYE,
look: OVERLOOK_LOOK,
minute: 0,
@@ -139,6 +158,7 @@ pub(super) const SCENARIOS: &[Scenario] = &[
},
Scenario {
name: "canal-noon",
map: MAP_AZEROTH,
eye: CANAL_EYE,
look: CANAL_LOOK,
minute: 720,
@@ -146,6 +166,7 @@ pub(super) const SCENARIOS: &[Scenario] = &[
},
Scenario {
name: "canal-night",
map: MAP_AZEROTH,
eye: CANAL_EYE,
look: CANAL_LOOK,
minute: 0,
@@ -153,6 +174,7 @@ pub(super) const SCENARIOS: &[Scenario] = &[
},
Scenario {
name: "water-noon",
map: MAP_AZEROTH,
eye: WATER_EYE,
look: WATER_LOOK,
minute: 720,
@@ -160,11 +182,41 @@ pub(super) const SCENARIOS: &[Scenario] = &[
},
Scenario {
name: "water-night",
map: MAP_AZEROTH,
eye: WATER_EYE,
look: WATER_LOOK,
minute: 0,
ui: None,
},
Scenario {
name: "inn-noon",
map: MAP_AZEROTH,
eye: INN_EYE,
look: INN_LOOK,
minute: 720,
ui: None,
},
// No `inn-night`: MEASURED at MAE 0.198 against `inn-noon`, 0.82 % of pixels — the room is lit
// by its hearth and candles, and the clock barely reaches it (the outdoor spots' own noon/night
// pairs differ by MAE 38-83 across ~100 % of the frame). A second window for 0.8 % of one shot
// is the low-value repetition 0632 exists to prevent, and noon is the richer of the two anyway:
// it has the daylight window AND the fire. Re-add it the day the interior light path earns it.
Scenario {
name: "felwood-noon",
map: MAP_KALIMDOR,
eye: FELWOOD_EYE,
look: FELWOOD_LOOK,
minute: 720,
ui: None,
},
Scenario {
name: "felwood-night",
map: MAP_KALIMDOR,
eye: FELWOOD_EYE,
look: FELWOOD_LOOK,
minute: 0,
ui: None,
},
];
/// Director shot 1 — the Northshire overlook, north-east of the Abbey looking at it: terrain,
@@ -178,17 +230,44 @@ pub(super) const CANAL_EYE: [f32; 3] = [-8871.7, 724.6, 110.7];
pub(super) const CANAL_LOOK: [f32; 3] = [-8836.7, 760.3, 112.3];
/// Director shot 3 — Elwynn river, south-east of Northshire: open water dominant, shoreline blend,
/// murloc camp, fog. Chosen by the director over the Goldshire inn interior, which stays in
/// [`ON_DEMAND`] as the interior-path fixture.
/// murloc camp, fog.
pub(super) const WATER_EYE: [f32; 3] = [-9527.0, -310.6, 70.8];
pub(super) const WATER_LOOK: [f32; 3] = [-9499.4, -351.3, 61.4];
/// Director shot 4 (decision 0743) — INSIDE the Lion's Pride Inn at Goldshire, standing in the
/// common room: the hearth (the MOCV-alpha self-illum bake), a daylight window, the lit chandelier,
/// the stair run, floor boards and ceiling beams, and a room full of props. The sweep's only
/// interior, and the only shot that exercises portal culling, the INT bake, the MOLT point pools
/// and WMO props at all — the surface the ledger's dungeon reports live on, and the one B101's
/// prop-cull regression (which hid the Blackrock lava falls) crossed unseen.
///
/// The agent's own interior framing (`inn-interior`, a tight crop of the kitchen hearth) was judged
/// badly framed and stays in [`ON_DEMAND`]; this is the director's, and it frames the ROOM.
///
/// NB the camera must stand over a FLOOR FACE: over a floorless pocket the portal cull's down-ray
/// reads "outside" and faithfully culls the containing group, which vanishes the room.
pub(super) const INN_EYE: [f32; 3] = [-9471.4, 39.4, 59.9];
pub(super) const INN_LOOK: [f32; 3] = [-9458.8, -7.5, 48.2];
/// Director shot 5 (decision 0743) — a Felwood hollow on **Kalimdor** (`MAP_KALIMDOR`): the
/// corrupted forest floor's root mat, a stand of emissive `felwoodmushroom` doodads, a pool of
/// green sludge (a liquid type no other golden shot contains), the vast trunks behind, and the
/// zone's sick-green fog and light palette.
///
/// The sweep's only shot outside Azeroth and outside the Elwynn/Stormwind palette — until this one
/// landed, a fog, tileset, WDL-horizon or per-map lighting regression anywhere else in the world
/// had nowhere to show up. It is also what forced [`Scenario::map`]: this spot's ADT tile
/// (`33_24`) exists in Azeroth too, empty, so on the old table it would have photographed a void.
pub(super) const FELWOOD_EYE: [f32; 3] = [4060.9, -944.3, 256.8];
pub(super) const FELWOOD_LOOK: [f32; 3] = [4014.0, -954.4, 242.9];
/// Every remaining named viewpoint — the UI look-pass fixtures, the sun/moon/sky regression
/// fixtures, the house-compass and street scenes. Capturable by name (`WOW_CAPTURE=<name>`) for
/// debugging and look passes, but NOT part of the blessed baseline sweep.
pub(super) const ON_DEMAND: &[Scenario] = &[
Scenario {
name: "house-north",
map: MAP_AZEROTH,
eye: HOUSE_EYE,
look: [-9389.1, 71.2, 58.0],
minute: 720,
@@ -196,6 +275,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
},
Scenario {
name: "house-south",
map: MAP_AZEROTH,
eye: HOUSE_EYE,
look: [-9489.1, 71.2, 58.0],
minute: 720,
@@ -203,6 +283,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
},
Scenario {
name: "house-west",
map: MAP_AZEROTH,
eye: HOUSE_EYE,
look: [-9439.1, 121.2, 58.0],
minute: 720,
@@ -210,6 +291,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
},
Scenario {
name: "house-east",
map: MAP_AZEROTH,
eye: HOUSE_EYE,
look: [-9439.1, 21.2, 58.0],
minute: 720,
@@ -225,6 +307,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
// client does the same — the audit's "faithful-cull residue"), which vanishes the room.
Scenario {
name: "inn-interior",
map: MAP_AZEROTH,
eye: [-9463.3, 4.4, 58.8],
look: [-9462.1, -5.6, 58.5],
minute: 720,
@@ -235,6 +318,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
// stays as the warm-light/fog fixture.
Scenario {
name: "northshire-dusk",
map: MAP_AZEROTH,
eye: GROUND_EYE,
look: GROUND_LOOK,
minute: 1170, // 19:30 — warm dusk light + fog
@@ -242,6 +326,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
},
Scenario {
name: "northshire-sky-noon",
map: MAP_AZEROTH,
eye: SKY_EYE,
look: SKY_LOOK,
minute: 720, // day sky-dome gradient + fog horizon
@@ -249,6 +334,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
},
Scenario {
name: "northshire-sky-dusk",
map: MAP_AZEROTH,
eye: SKY_EYE,
look: SKY_LOOK,
minute: 1170, // dusk dome warp + low sun + stars emerging
@@ -262,6 +348,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
// was cut along a giant faceted circle).
Scenario {
name: "northshire-sun-flare",
map: MAP_AZEROTH,
eye: SKY_EYE,
look: [-8797.0, 23.0, 264.0], // eye + 300·(elev 30°, az 45°) — the sun's spot at 17:30
minute: 1050,
@@ -276,6 +363,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
// unit tests (`flare_ray_*`) and the sun fixtures.
Scenario {
name: "northshire-moonrise",
map: MAP_AZEROTH,
eye: SKY_EYE,
look: [-8775.0, 45.0, 190.0], // eye + 300·(elev 15°, az 45°) — the moon's spot at 22:44
minute: 1364,
@@ -287,6 +375,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
// the moonrise fixture above proves its absence early, this one its presence at depth of night.
Scenario {
name: "northshire-moon-halo",
map: MAP_AZEROTH,
eye: SKY_EYE,
look: [-8858.0, -38.0, 358.0], // eye + 300·(elev 55°, az 45°) — the moon's spot at 00:00
minute: 0,
@@ -298,6 +387,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
// load is resident here; Northshire scenes never exercise that scale.
Scenario {
name: "stormwind",
map: MAP_AZEROTH,
eye: [-8833.38, 628.63, 96.0],
look: [-8809.1, 672.3, 94.0],
minute: 720,
@@ -309,6 +399,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
// director look pass, so "looks like shit" gets caught in-loop, not on the director's screen.
Scenario {
name: "ui-merchant",
map: MAP_AZEROTH,
eye: GROUND_EYE,
look: GROUND_LOOK,
minute: 720,
@@ -316,6 +407,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
},
Scenario {
name: "ui-gossip",
map: MAP_AZEROTH,
eye: GROUND_EYE,
look: GROUND_LOOK,
minute: 720,
@@ -323,6 +415,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
},
Scenario {
name: "ui-bank",
map: MAP_AZEROTH,
eye: GROUND_EYE,
look: GROUND_LOOK,
minute: 720,
@@ -330,6 +423,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
},
Scenario {
name: "ui-quest",
map: MAP_AZEROTH,
eye: GROUND_EYE,
look: GROUND_LOOK,
minute: 720,
@@ -337,6 +431,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
},
Scenario {
name: "ui-questgreeting",
map: MAP_AZEROTH,
eye: GROUND_EYE,
look: GROUND_LOOK,
minute: 720,
@@ -344,6 +439,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
},
Scenario {
name: "ui-questlog",
map: MAP_AZEROTH,
eye: GROUND_EYE,
look: GROUND_LOOK,
minute: 720,
@@ -351,6 +447,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
},
Scenario {
name: "ui-loot",
map: MAP_AZEROTH,
eye: GROUND_EYE,
look: GROUND_LOOK,
minute: 720,
@@ -358,6 +455,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
},
Scenario {
name: "ui-bag",
map: MAP_AZEROTH,
eye: GROUND_EYE,
look: GROUND_LOOK,
minute: 720,
@@ -367,6 +465,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-tooltip`.
Scenario {
name: "ui-tooltip",
map: MAP_AZEROTH,
eye: GROUND_EYE,
look: GROUND_LOOK,
minute: 720,
@@ -376,6 +475,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
// seeded hostile wolf. Run with `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-tooltip-world`.
Scenario {
name: "ui-tooltip-world",
map: MAP_AZEROTH,
eye: GROUND_EYE,
look: GROUND_LOOK,
minute: 720,
@@ -385,6 +485,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-char`.
Scenario {
name: "ui-char",
map: MAP_AZEROTH,
eye: GROUND_EYE,
look: GROUND_LOOK,
minute: 720,
@@ -395,6 +496,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-unitframes`.
Scenario {
name: "ui-unitframes",
map: MAP_AZEROTH,
eye: GROUND_EYE,
look: GROUND_LOOK,
minute: 720,
@@ -407,6 +509,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-actionbar`.
Scenario {
name: "ui-actionbar",
map: MAP_AZEROTH,
eye: GROUND_EYE,
look: GROUND_LOOK,
minute: 720,
@@ -418,6 +521,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
// `WOW_CAPTURE=vplates` (main.rs sizes this window 1024×768 — the 1:1 gx window).
Scenario {
name: "vplates",
map: MAP_AZEROTH,
eye: [-8956.5, -137.5, 85.6],
look: [-8949.95, -132.49, 84.8],
minute: 720,
@@ -428,6 +532,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-worldmap`.
Scenario {
name: "ui-worldmap",
map: MAP_AZEROTH,
eye: GROUND_EYE,
look: GROUND_LOOK,
minute: 720,
@@ -436,6 +541,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
// The spellbook over a seeded mage book. Run with `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-spellbook`.
Scenario {
name: "ui-spellbook",
map: MAP_AZEROTH,
eye: GROUND_EYE,
look: GROUND_LOOK,
minute: 720,
@@ -445,6 +551,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-chatedit`.
Scenario {
name: "ui-chatedit",
map: MAP_AZEROTH,
eye: GROUND_EYE,
look: GROUND_LOOK,
minute: 720,
@@ -455,6 +562,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
// out in the water. Run with `WOW_CAPTURE=name-water`.
Scenario {
name: "name-water",
map: MAP_AZEROTH,
eye: WATER_EYE,
look: WATER_LOOK,
minute: 720,
+6 -3
View File
@@ -212,12 +212,12 @@ use world_map::WorldMapPlugin;
/// logs in — so the player sits in the middle of the block instead of Stormwind's edge.
const SPAWN_XY: (f32, f32) = (-8949.95, -132.49);
fn main() {
fn main() -> AppExit {
// `WOW_CAPTURE=list` just prints the harness scenario names (the source of truth `scripts/visual.sh`
// reads) and exits before any window/asset setup.
if std::env::var("WOW_CAPTURE").as_deref() == Ok("list") {
capture::print_scenario_names();
return;
return AppExit::Success;
}
let mut app = App::new();
@@ -779,5 +779,8 @@ fn main() {
app.add_plugins(perf::FpsJournalPlugin);
}
app.run();
// Return the app's own exit status instead of dropping it: a failed capture writes
// `AppExit::error()` (see `capture::drive_capture`), and discarding it made the process exit 0
// with no PNG on disk — which is how a sweep carried on around a missing shot (decision 0743).
app.run()
}
+4
View File
@@ -93,6 +93,10 @@ fn load_world_map(mut commands: Commands, world_assets: Option<Res<WorldAssets>>
// `player::wire_in` overwrites this from the server's world-verify the moment it says
// where we are. Without it every headless capture is stuck in Eastern Kingdoms, and a
// horizon report from anywhere else cannot be reproduced.
//
// A named capture scenario carries its own map (`capture::scenarios::Scenario::map`)
// and writes it here before this runs, so the golden sweep's Kalimdor shots need no env
// at all — see `capture::CapturePlugin::build` (decision 0743).
let map = std::env::var("WOW_MAP")
.ok()
.and_then(|v| v.parse().ok())