Desktop self test: DC_SHOT starts the demo map or a savegame, takes a screenshot and quits

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 00:28:23 +01:00
1 parent d7b6d6de7e
commit 04d9d719d7
1 file changed
+98
+98
View File
@@ -945,6 +945,8 @@ async fn main() {
#[cfg(not(target_arch = "wasm32"))]
audio::native::start();
#[cfg(not(target_arch = "wasm32"))]
let mut shot = ShotPlan::from_env();
loop {
#[cfg(not(target_arch = "wasm32"))]
audio::native::pump();
@@ -2160,12 +2162,108 @@ app.pending_briefing = briefing_of(&campaign, i);
was_briefing = briefing;
voice.update(now, if app.settings.muted { 0.0 } else { app.settings.volume });
#[cfg(not(target_arch = "wasm32"))]
let shot_now = shot.as_mut().and_then(|p| p.before_frame(&mut app, &mut ui, &mut menu, &mut r3d, now));
draw_frame(&mut app, &gfx, &mut r3d, &mut ui, &mut menu, &cloud, &inp, (mx, my), touch_mode, now);
ui::end_frame();
#[cfg(not(target_arch = "wasm32"))]
if let Some(path) = shot_now {
shot_capture(&path);
}
next_frame().await
}
}
// ---- self test and screenshots (desktop): DC_SHOT=demo or a savegame file starts it right
// away, DC_SHOT_STYLE=hd|pixel picks the graphics style (not saved), DC_SHOT_AFTER seconds later
// (default 10) the screen goes to DC_SHOT_OUT (default /tmp/dc-shot.png) and the game ends;
// DC_SHOT_ZOOM sets the camera zoom, DC_SHOT_YAW its turn, DC_SHOT_AT=x,z where it looks
#[cfg(not(target_arch = "wasm32"))]
struct ShotPlan {
load: String,
out: String,
after: f64,
style: Option<u16>,
zoom: Option<f32>,
yaw: Option<f32>,
at: Option<(f32, f32)>,
started: Option<f64>,
}
#[cfg(not(target_arch = "wasm32"))]
impl ShotPlan {
fn from_env() -> Option<ShotPlan> {
let load = std::env::var("DC_SHOT").ok()?;
let num = |k: &str| std::env::var(k).ok().and_then(|v| v.parse::<f32>().ok());
Some(ShotPlan {
load,
out: std::env::var("DC_SHOT_OUT").unwrap_or_else(|_| "/tmp/dc-shot.png".into()),
after: num("DC_SHOT_AFTER").unwrap_or(10.0) as f64,
style: std::env::var("DC_SHOT_STYLE").ok().map(|v| if v == "pixel" { settings::PIXEL_ART_ROWS } else { settings::HIGH_RES_ROWS }),
zoom: num("DC_SHOT_ZOOM"),
yaw: num("DC_SHOT_YAW"),
at: std::env::var("DC_SHOT_AT").ok().and_then(|v| {
let (a, b) = v.split_once(',')?;
Some((a.trim().parse().ok()?, b.trim().parse().ok()?))
}),
started: None,
})
}
// starts the scene on the first frame; returns the file to write once the time has come
fn before_frame(&mut self, app: &mut App, ui: &mut Ui, menu: &mut Menu, r3d: &mut R3d, now: f64) -> Option<String> {
match self.started {
None => {
if let Some(rows) = self.style {
app.settings.pixel_rows = rows;
}
app.settings.hints = false;
apply_settings(&app.settings, r3d);
if self.load == "demo" {
app.seed = next_seed(app.seed);
(app.game, app.cam) = Game::new_demo_map(app.seed);
apply_game_settings(&mut app.game, &app.settings);
r3d.invalidate();
} else if let Ok(bytes) = std::fs::read(&self.load) {
app.load_bytes(&bytes, ui, menu, r3d, now);
}
menu.page = None;
ui.show_help = false;
if let Some(z) = self.zoom {
app.cam.zoom = z;
}
if let Some(y) = self.yaw {
app.cam.yaw = y;
}
if let Some((x, z)) = self.at {
(app.cam.tx, app.cam.tz, app.cam.rx, app.cam.rz) = (x, z, x, z);
}
self.started = Some(now);
None
}
Some(t0) if now - t0 >= self.after => Some(self.out.clone()),
_ => None,
}
}
}
// writes the screen to a PNG file and ends the game
#[cfg(not(target_arch = "wasm32"))]
fn shot_capture(path: &str) {
let img = get_screen_data();
let (w, h) = (img.width as u32, img.height as u32);
let mut out = Image::gen_image_color(w as u16, h as u16, BLACK);
for y in 0..h {
for x in 0..w {
let mut p = img.get_pixel(x, y);
p.a = 1.0;
out.set_pixel(x, y, p);
}
}
out.export_png(path);
std::process::exit(0);
}
#[allow(clippy::too_many_arguments)]
// pointer: CSS pixels of the mouse or finger; touch: hover highlight and information only
// while a finger is down or for a long-pressed point