diff options
Diffstat (limited to 'server/replaytool')
| -rw-r--r-- | server/replaytool/Cargo.toml | 4 | ||||
| -rw-r--r-- | server/replaytool/src/main.rs | 81 | ||||
| -rw-r--r-- | server/replaytool/src/record.rs | 45 | ||||
| -rw-r--r-- | server/replaytool/src/render.rs | 46 | ||||
| -rw-r--r-- | server/replaytool/src/replay.rs | 7 |
5 files changed, 148 insertions, 35 deletions
diff --git a/server/replaytool/Cargo.toml b/server/replaytool/Cargo.toml index cfc50b3a..b3055704 100644 --- a/server/replaytool/Cargo.toml +++ b/server/replaytool/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "hurrycurry-replaytool" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] log = "0.4.28" @@ -14,9 +14,9 @@ tokio-tungstenite = { version = "0.27.0", features = [ "rustls-tls-native-roots", ] } futures-util = "0.3.31" -rand = "0.9.2" clap = { version = "4.5.47", features = ["derive"] } async-compression = { version = "0.4.30", features = ["zstd", "tokio"] } rustls = { version = "0.23.31", features = ["ring"] } hurrycurry-protocol = { path = "../protocol" } +rand = "0.10.0" diff --git a/server/replaytool/src/main.rs b/server/replaytool/src/main.rs index 1e5be601..4785a99a 100644 --- a/server/replaytool/src/main.rs +++ b/server/replaytool/src/main.rs @@ -1,6 +1,6 @@ /* Hurry Curry! - a game about cooking - Copyright (C) 2025 Hurry Curry! Contributors + Copyright (C) 2026 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 @@ -15,7 +15,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -#![feature(exit_status_error)] pub mod record; pub mod render; @@ -23,18 +22,21 @@ pub mod replay; use crate::{ record::record, - render::{render, RenderArgs}, + render::{RenderArgs, render}, replay::replay, }; +use anyhow::Context; use clap::Parser; -use hurrycurry_protocol::PacketC; -use log::{info, warn, LevelFilter}; +use futures_util::{SinkExt, StreamExt}; +use hurrycurry_protocol::{Character, Message, PacketC, PacketS, PlayerClass}; +use log::{LevelFilter, debug, error, info, warn}; use serde::{Deserialize, Serialize}; use std::{ path::PathBuf, time::{Duration, SystemTime}, }; use tokio::{net::TcpListener, time::sleep}; +use tokio_tungstenite::tungstenite::{self}; #[derive(Parser)] enum Args { @@ -47,9 +49,15 @@ enum Args { output: PathBuf, }, /// Starts a local server that replays previously recorded sessions - Replay { input: PathBuf }, + Replay { + input: PathBuf, + }, /// Runs a replay server and the client in movie mode to record that replay Render(#[command(flatten)] RenderArgs), + DownloadMap { + url: String, + mapname: String, + }, } #[derive(Serialize, Deserialize)] @@ -89,7 +97,7 @@ async fn main() -> anyhow::Result<()> { output.clone() }; if let Err(e) = record(&out, &url).await { - warn!("recording failed: {e}"); + warn!("recording failed: {e:#}"); sleep(Duration::from_secs(1)).await; } if r#loop { @@ -109,6 +117,65 @@ async fn main() -> anyhow::Result<()> { Args::Render(a) => { render(a).await?; } + Args::DownloadMap { url, mapname } => { + download_map(&url, &mapname).await?; + } + } + Ok(()) +} + +pub async fn download_map(url: &str, mapname: &str) -> anyhow::Result<()> { + let (mut sock, _) = tokio_tungstenite::connect_async(url).await?; + + sock.send(tungstenite::Message::Text( + serde_json::to_string(&PacketS::Join { + name: "map downloader".to_string(), + character: Character::default(), + class: PlayerClass::Chef, + id: None, + position: None, + }) + .unwrap() + .into(), + )) + .await?; + + while let Some(Ok(message)) = sock.next().await { + match message { + tungstenite::Message::Text(line) => { + let packet: PacketC = serde_json::from_str(&line).context("invalid packet")?; + debug!("<- {packet:?}"); + + match packet { + PacketC::Joined { id } => { + sock.send(tungstenite::Message::Text( + serde_json::to_string(&PacketC::Communicate { + player: id, + message: Some(Message::Text(format!("/download map {mapname}"))), + timeout: None, + }) + .unwrap() + .into(), + )) + .await?; + } + PacketC::ServerMessage { + message: Message::Text(t), + .. + } => { + println!("{t}"); + return Ok(()); + } + _ => (), + } + } + tungstenite::Message::Close(_) => { + error!("connection closed by server"); + break; + } + _ => (), + } } + Ok(()) } diff --git a/server/replaytool/src/record.rs b/server/replaytool/src/record.rs index 8c12fc94..e51db3f8 100644 --- a/server/replaytool/src/record.rs +++ b/server/replaytool/src/record.rs @@ -1,6 +1,6 @@ /* Hurry Curry! - a game about cooking - Copyright (C) 2025 Hurry Curry! Contributors + Copyright (C) 2026 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 @@ -16,19 +16,22 @@ */ +use crate::Event; use anyhow::Context; use async_compression::tokio::write::ZstdEncoder; -use futures_util::StreamExt; -use hurrycurry_protocol::PacketC; -use log::{debug, info}; -use std::{path::Path, time::Instant}; +use futures_util::{SinkExt, StreamExt}; +use hurrycurry_protocol::{PacketC, PacketS}; +use log::{debug, error, info}; +use std::{ + io::{Write, stderr}, + path::Path, + time::Instant, +}; use tokio::{ fs::File, io::{AsyncWriteExt, BufWriter}, }; -use tokio_tungstenite::tungstenite; - -use crate::Event; +use tokio_tungstenite::tungstenite::{self, Message}; pub async fn record(output: &Path, url: &str) -> anyhow::Result<()> { let mut file = BufWriter::new(ZstdEncoder::new(BufWriter::new( @@ -38,6 +41,8 @@ pub async fn record(output: &Path, url: &str) -> anyhow::Result<()> { let (mut sock, _) = tokio_tungstenite::connect_async(url).await?; info!("starting recording."); let start = Instant::now(); + let mut counter = 0; + let mut last_keepalive = Instant::now(); while let Some(Ok(message)) = sock.next().await { match message { @@ -46,6 +51,21 @@ pub async fn record(output: &Path, url: &str) -> anyhow::Result<()> { debug!("<- {packet:?}"); let is_end = matches!(packet, PacketC::SetIngame { state: false, .. }); + let is_start = matches!(packet, PacketC::SetIngame { state: true, .. }); + + if is_start { + sock.send(Message::Text( + serde_json::to_string(&PacketS::Ready).unwrap().into(), + )) + .await?; + } + if last_keepalive.elapsed().as_secs_f32() > 1. { + last_keepalive = Instant::now(); + sock.send(Message::Text( + serde_json::to_string(&PacketS::Keepalive).unwrap().into(), + )) + .await?; + } file.write_all( format!( @@ -60,12 +80,19 @@ pub async fn record(output: &Path, url: &str) -> anyhow::Result<()> { ) .await?; + counter += 1; + eprint!("{counter} packets received\r"); + stderr().flush().unwrap(); + if is_end { info!("stopping replay..."); break; } } - tungstenite::Message::Close(_) => break, + tungstenite::Message::Close(_) => { + error!("connection closed by server"); + break; + } _ => (), } } diff --git a/server/replaytool/src/render.rs b/server/replaytool/src/render.rs index 2314de79..ccd97da5 100644 --- a/server/replaytool/src/render.rs +++ b/server/replaytool/src/render.rs @@ -1,6 +1,6 @@ /* Hurry Curry! - a game about cooking - Copyright (C) 2025 Hurry Curry! Contributors + Copyright (C) 2026 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 @@ -17,12 +17,11 @@ */ use crate::replay::replay; -use anyhow::{anyhow, Result}; +use anyhow::{Context, Result, anyhow, bail}; use log::info; -use rand::random; use std::{path::PathBuf, str::FromStr}; use tokio::{ - fs::{create_dir_all, remove_dir, remove_file, File}, + fs::{File, create_dir_all, remove_dir, remove_file}, io::AsyncWriteExt, net::TcpListener, process::Command, @@ -32,6 +31,9 @@ use tokio::{ pub struct RenderArgs { /// Replay file path input: PathBuf, + /// Camera replay file path + #[arg(short = 'c', long)] + camera: Option<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")] @@ -49,15 +51,17 @@ pub struct RenderArgs { 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 port = 27090; - let ws_listener = TcpListener::bind(("127.0.0.1", port)).await?; + 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::<u64>())); + let cwd = PathBuf::from_str("/tmp").unwrap().join(format!( + "hurrycurry-render-cfg-{:016x}", + rand::random::<u64>() + )); let config = { let (width, height) = a @@ -80,9 +84,13 @@ window/size/viewport_height={height} .await?; #[cfg(unix)] - tokio::fs::symlink(&a.client_pck, cwd.join("client.pck")).await?; + 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?; + tokio::fs::copy(&a.client_pck, cwd.join("client.pck")) + .await + .context("copying pck")?; let mut args = Vec::new(); if a.headless { @@ -107,16 +115,26 @@ window/size/viewport_height={height} args.extend(["--fixed-fps", &fps]); args.push("--print-fps"); args.push("--"); - let uri = format!("ws://127.0.0.1:{port}"); + 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()?; + 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()?; + let e = client.wait().await?; + if !e.success() { + bail!("failed with code {:?}", e.code()) + } remove_file(cwd.join("override.cfg")).await?; remove_file(cwd.join("client.pck")).await?; diff --git a/server/replaytool/src/replay.rs b/server/replaytool/src/replay.rs index d442f599..92da6140 100644 --- a/server/replaytool/src/replay.rs +++ b/server/replaytool/src/replay.rs @@ -1,6 +1,6 @@ /* Hurry Curry! - a game about cooking - Copyright (C) 2025 Hurry Curry! Contributors + Copyright (C) 2026 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 @@ -17,7 +17,7 @@ */ use crate::Event; -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use async_compression::tokio::bufread::ZstdDecoder; use futures_util::{SinkExt, StreamExt}; use hurrycurry_protocol::{Message, PacketC, PacketS}; @@ -109,12 +109,13 @@ pub async fn replay(ws_listener: &TcpListener, input: &Path) -> Result<()> { send(PacketC::ReplayStop).await?; next_event = Event { ts: f64::INFINITY, - packet: PacketC::FlushMap, + packet: PacketC::ReplayStop, }; continue; }; } } + PacketS::Keepalive => (), x => warn!("unhandled client packet: {x:?}"), } } |