/* Hurry Curry! - a game about cooking Copyright (C) 2025 Hurry Curry! Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License only. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ use crate::replay::replay; use anyhow::{anyhow, Result}; use log::info; use rand::random; use std::{path::PathBuf, str::FromStr}; use tokio::{ fs::{create_dir_all, remove_dir, remove_file, File}, io::AsyncWriteExt, net::TcpListener, process::Command, }; #[derive(clap::Parser)] pub struct RenderArgs { /// Replay file path input: PathBuf, /// Output video file path passed to godot (must end in either .avi for MJPEG or .png for PNG sequence) output: PathBuf, #[arg(short = 'r', long, default_value = "30")] framerate: usize, #[arg(short = 'R', long, default_value = "1280x720")] resolution: String, /// Render without display server; Requires wlheadless-run and mutter #[arg(short = 'H', long)] headless: bool, #[arg(long, default_value = "wayland")] video_driver: String, #[arg(long, default_value = "vulkan")] rendering_driver: String, #[arg(long, default_value = "mutter")] headless_compositor: String, #[arg(long, default_value = "/usr/share/hurrycurry/client.pck")] client_pck: PathBuf, } pub async fn render(a: RenderArgs) -> Result<()> { let port = 27090; let ws_listener = TcpListener::bind(("127.0.0.1", port)).await?; let cwd = PathBuf::from_str("/tmp") .unwrap() .join(format!("hurrycurry-render-cfg-{:016x}", random::())); let config = { let (width, height) = a .resolution .split_once("x") .ok_or(anyhow!("resolution malformed"))?; format!( r#"config_version=5 [display] window/size/viewport_width={width} window/size/viewport_height={height} "#, ) }; create_dir_all(&cwd).await?; File::create(cwd.join("override.cfg")) .await? .write_all(config.as_bytes()) .await?; #[cfg(unix)] tokio::fs::symlink(&a.client_pck, cwd.join("client.pck")).await?; #[cfg(not(unix))] tokio::fs::copy(&a.client_pck, cwd.join("client.pck")).await?; let mut args = Vec::new(); if a.headless { args.push("wlheadless-run"); args.extend(["-c", &a.headless_compositor]); args.push("--"); } if a.headless && a.video_driver == "x11" { args.push("xwayland-run"); args.push("--"); } args.push("godot"); let main_pack = cwd.join("client.pck"); let main_pack = main_pack.to_str().unwrap(); args.extend(["--main-pack", main_pack]); if a.headless { args.extend(["--video-driver", &a.video_driver]); args.extend(["--rendering-driver", &a.rendering_driver]); } args.extend(["--write-movie", a.output.to_str().unwrap()]); let fps = a.framerate.to_string(); args.extend(["--fixed-fps", &fps]); args.push("--print-fps"); args.push("--"); let uri = format!("ws://127.0.0.1:{port}"); args.push(&uri); info!("using commandline {:?}", args.join(" ")); let mut client = Command::new(args[0]).args(&args[1..]).spawn()?; info!("listening for websockets on {}", ws_listener.local_addr()?); replay(&ws_listener, &a.input).await?; client.wait().await?.exit_ok()?; remove_file(cwd.join("override.cfg")).await?; remove_file(cwd.join("client.pck")).await?; remove_dir(cwd).await?; Ok(()) }