capture: a setup slot, for the moment before the scene ages

$WOW_CAPTURE_SETUP runs a chunk at the first aging frame -- early enough
to change a setting or create a frame and still have the scene settle
around it. $WOW_CAPTURE_EVAL keeps the shutter, where a read and the
pixels it explains come from the same frame; anything created there is a
frame too late to be in the image.

The deep-zoom map shot needed it. OpenMapClassic resolves minimap tiles
by FileDataID by default and this client has none, so its own status
reports "loaded YES=0 NO=8"; it already carries a path mode for exactly
that, asking for textures\Minimap\<folder>\map<col>_<row>, of which this
install ships 10,884. Switching to it raised, because
S.ResetMinimapTiles reads lodPieces/lodPool sixteen lines before they are
declared and so bound them to nil globals -- an upstream bug, and the
only two callers are the two switches that reach the mode.

Port edits mirrored to Addon_Ports/openmap_port/: the declaration moved
above its use, and the tile mode defaulted to the one that can work here.
Captured at zoom 40: the minimap's terrain draws through the map.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-11-03 17:03:21 -07:00
co-authored by Claude Opus 5
parent 62c019e744
commit 2281179874
2 changed files with 57 additions and 8 deletions
+18 -8
View File
@@ -869,32 +869,39 @@ struct LateCapture<'w> {
script: Option<NonSendMut<'w, benilla_ui::script::UiScript>>,
}
/// `$WOW_CAPTURE_EVAL=<lua>`: run a chunk in the capture's own VM at the shutter and log what it
/// answers, as `capture eval:` lines.
/// Run a chunk in the capture's own VM and log what it answers, as `capture <label>:` lines.
///
/// Two slots, because a capture has exactly two moments worth reaching:
///
/// * **`$WOW_CAPTURE_SETUP`**, at the first aging frame — early enough to change a setting, open
/// something or create a frame, and still have the scene settle around it before the shutter.
/// * **`$WOW_CAPTURE_EVAL`**, at the shutter — the settled state, so a number and the pixels it
/// explains come from the same frame. Reads only: a frame created here is one frame too late
/// to be in the image.
///
/// A capture is the only harness that has both a renderer and a player's interface, and it had no
/// way to be *asked* anything — every question about what a shot shows had to be answered by
/// looking at the picture. A chunk here is read at the one instant the image is taken, so a number
/// and the pixels it explains come from the same frame. `\n` in the answer is split across lines,
/// so a probe can return a block.
fn capture_eval(script: Option<NonSendMut<benilla_ui::script::UiScript>>) {
let Ok(chunk) = std::env::var("WOW_CAPTURE_EVAL") else {
fn capture_lua(var: &str, label: &str, script: Option<NonSendMut<benilla_ui::script::UiScript>>) {
let Ok(chunk) = std::env::var(var) else {
return;
};
if chunk.is_empty() {
return;
}
let Some(script) = script else {
warn!("capture eval: no interface in this capture, nothing to ask");
warn!("capture {label}: no interface in this capture, nothing to ask");
return;
};
match script.eval::<String>(&chunk) {
Ok(out) => {
for line in out.lines() {
info!("capture eval: {line}");
info!("capture {label}: {line}");
}
}
Err(e) => warn!("capture eval: raised: {e}"),
Err(e) => warn!("capture {label}: raised: {e}"),
}
}
@@ -1050,6 +1057,9 @@ fn drive_capture(
}
}
Phase::Aging(n) => {
if n == 0 {
capture_lua("WOW_CAPTURE_SETUP", "setup", late.script.take());
}
// No restart on late arrivals: the image already stopped changing before release.
if n + 1 < age_frames() {
Phase::Aging(n + 1)
@@ -1078,7 +1088,7 @@ fn drive_capture(
);
Phase::ProbeWarmup(0)
} else {
capture_eval(late.script.take());
capture_lua("WOW_CAPTURE_EVAL", "eval", late.script.take());
commands
.spawn(Screenshot::primary_window())
.observe(save_to_disk(ctx.out.clone()));
+39
View File
@@ -2951,3 +2951,42 @@ The palette is now read as "as much as the file has", and an index past the end
black, which is what an unused index means. `every_world_map_texture_in_the_chain_decodes` walks
the whole folder: **3553 textures, 7 failures before, 0 after.** It fails on the old decoder,
which is the only reason to trust it.
## The deep zoom: the minimap over the map
"Zoom in all the way into the minimap mapped out over the map" was one of the two things grom
named. OpenMapClassic already has the mechanism for a client without FileDataIDs — `/omc tilemode`
picks between `fdid` (Forever's numeric ids) and `twm`, which asks for
`textures\Minimap\<folder>\map<col>_<row>`. This install ships **10,884** of those. `/omc status`
on the fdid default reports `terrain sample summary: loaded YES=0 NO=8`, which is what a client
with no FileDataIDs has to report.
Switching it raised:
```
Close.lua:196: bad argument #1 to `pairs' (table expected, got nil)
```
`local lodPieces, lodPool, wantedLOD` is declared at line 212 and read by `S.ResetMinimapTiles` at
196. Lua binds a name at compile time, so inside that function the three resolved to the
**globals** of those names — nil — and every call raised. Its only two callers are `/omc tilemode`
and `/omc layer close|lod`, so the one switch this client needs could not be thrown. An upstream
bug; the port edit moves the declaration above the function.
Two port edits, both mirrored at `Addon_Ports/openmap_port/`:
- `Map/Close.lua:194` — the declaration moved above its use.
- `Map/Close.lua:26` — `minimapTileMode` defaults to `"twm"`. Not a preference: `fdid` cannot
resolve a tile on a client that has no FileDataIDs. The blend itself stays off by default,
which is upstream's choice (`/omc minimap on`).
Captured at zoom 40 with the blend on: the minimap's own terrain draws through the map's
parchment, roads and ridges and all, under the zone's labels.
### A second slot on the capture harness
`$WOW_CAPTURE_SETUP=<lua>` runs at the **first aging frame**, early enough to change a setting,
open something or create a frame and still have the scene settle around it. `$WOW_CAPTURE_EVAL`
keeps the shutter, where a read and the pixels it explains come from the same frame. The deep-zoom
shot needed the first: the tile mode has to be switched before anything draws, and a chunk at the
shutter is a frame too late to appear in the image.