aboutsummaryrefslogtreecommitdiff
path: root/server/replaytool
diff options
context:
space:
mode:
Diffstat (limited to 'server/replaytool')
-rw-r--r--server/replaytool/Cargo.toml4
-rw-r--r--server/replaytool/src/main.rs81
-rw-r--r--server/replaytool/src/record.rs45
-rw-r--r--server/replaytool/src/render.rs46
-rw-r--r--server/replaytool/src/replay.rs7
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:?}"),
}
}