/* 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::{Context, Result, anyhow}; use log::info; use std::{path::PathBuf, random::random, str::FromStr}; use tokio::{ fs::{File, create_dir_all, remove_dir, remove_file}, io::AsyncWriteExt, net::TcpListener, process::Command, }; #[derive(clap::Parser)] pub struct RenderArgs { /// Replay file path input: PathBuf, /// Camera replay file path #[arg(short = 'c', long)] camera: Option, /// 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, #[arg(long, default_value = "27090")] port: u16, } pub async fn render(a: RenderArgs) -> Result<()> { let ws_listener = TcpListener::bind(("127.0.0.1", a.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 .context("symlinking pck")?; #[cfg(not(unix))] tokio::fs::copy(&a.client_pck, cwd.join("client.pck")) .await .context("copying pck")?; 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 camera_path = a.camera.map(|path| path.to_str().unwrap().to_string()); if let Some(path) = &camera_path { args.extend(["--replay-camera", path]); } let uri = format!("ws://127.0.0.1:{}", a.port); args.push(&uri); info!("using commandline {:?}", args.join(" ")); let mut client = Command::new(args[0]) .args(&args[1..]) .spawn() .context("starting command")?; 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(()) }