/* Undercooked - a game about cooking Copyright 2024 metamuffin 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 anyhow::Result; use futures_util::{SinkExt, StreamExt}; use log::{debug, info, warn}; use std::{sync::Arc, time::Duration}; use tokio::{ net::TcpListener, spawn, sync::{broadcast, mpsc::channel, RwLock}, time::sleep, }; use tokio_tungstenite::tungstenite::Message; use undercooked::{ customer::customer, game::Game, load_gamedata, protocol::{PacketC, PacketS, PlayerID}, }; #[tokio::main] async fn main() -> Result<()> { env_logger::init_from_env("LOG"); let ws_listener = TcpListener::bind("0.0.0.0:27032").await?; info!("listening for websockets on {}", ws_listener.local_addr()?); let data = load_gamedata(); let game = Arc::new(RwLock::new(Game::new(data.into()))); let (tx, rx) = broadcast::channel::(1024); { let game = game.clone(); spawn(async move { let dt = 1. / 25.; loop { { let mut g = game.write().await; g.tick(dt); while let Some(p) = g.packet_out() { debug!("-> {p:?}"); let _ = tx.send(p); } } sleep(Duration::from_secs_f32(dt)).await; } }); } spawn(customer(game.clone(), rx.resubscribe())); for id in (1..).map(PlayerID) { let (sock, addr) = ws_listener.accept().await?; let Ok(sock) = tokio_tungstenite::accept_async(sock).await else { warn!("invalid ws handshake"); continue; }; let (mut write, mut read) = sock.split(); let game = game.clone(); let mut rx = rx.resubscribe(); let (error_tx, mut error_rx) = channel::(8); info!("{addr} connected via ws"); let init = game.write().await.prime_client(id); spawn(async move { for p in init { if let Err(e) = write .send(tokio_tungstenite::tungstenite::Message::Text( serde_json::to_string(&p).unwrap(), )) .await { warn!("ws error on init: {e}"); return; } } loop { let Some(packet) = tokio::select!( p = rx.recv() => p.ok(), p = error_rx.recv() => p, ) else { break; }; if let Err(e) = write .send(tokio_tungstenite::tungstenite::Message::Text( serde_json::to_string(&packet).unwrap(), )) .await { warn!("ws error: {e}"); break; } } }); spawn(async move { while let Some(Ok(message)) = read.next().await { match message { Message::Text(line) => { let Ok(packet): Result = serde_json::from_str(&line) else { warn!("invalid json over ws"); break; }; debug!("<- {id:?} {packet:?}"); if let Err(e) = game.write().await.packet_in(id, packet) { warn!("client error: {e}"); let _ = error_tx .send(PacketC::Error { message: format!("{e}"), }) .await; } } Message::Close(_) => break, _ => (), } } let _ = game.write().await.packet_in(id, PacketS::Leave); }); } Ok(()) }