From 04d9d719d72c3c6d27b0b2805711b1260b37a615 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Sun, 8 Nov 2026 00:28:23 +0100 Subject: [PATCH] Desktop self test: DC_SHOT starts the demo map or a savegame, takes a screenshot and quits Co-Authored-By: Claude Opus 5.5 --- src/main.rs | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/src/main.rs b/src/main.rs index 91efe0c..71d4705 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, + zoom: Option, + yaw: Option, + at: Option<(f32, f32)>, + started: Option, +} + +#[cfg(not(target_arch = "wasm32"))] +impl ShotPlan { + fn from_env() -> Option { + let load = std::env::var("DC_SHOT").ok()?; + let num = |k: &str| std::env::var(k).ok().and_then(|v| v.parse::().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 { + 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