use std::{ error::Error, net::SocketAddr, sync::Arc, collections::HashMap, num::NonZeroUsize, }; use futures::stream::StreamExt; mod types; mod conn; mod minesweeper; use types::*; use tokio::sync::RwLock; const FONT_FILE: &[u8] = include_bytes!("../assets/VT323-Regular.ttf"); const AREA_LIMIT: usize = 150*150; const ROOM_LIMIT: usize = 8; fn main() -> Result<(), Box> { let conf = Config { cert: "./cert.pem".to_owned(), pkey: "./cert.rsa".to_owned(), index_pg: "./assets/index.html".to_owned(), room_pg: "./assets/room.html".to_owned(), client_code: "./assets/client.js".to_owned(), stylesheet: "./assets/style.css".to_owned(), socket_addr: ([0,0,0,0],31235).into(), }; tokio_main(conf) } #[tokio::main] async fn tokio_main(conf: Config) -> Result<(), Box> { let rooms: RoomMap = Arc::new(RwLock::new(HashMap::new())); let public_rooms = Arc::new(RwLock::new(HashMap::new())); use warp::*; let index = path::end().and(fs::file(conf.index_pg.clone())); let style = path!("s.css").and(fs::file(conf.stylesheet.clone())); let code = path!("c.js").and(fs::file(conf.client_code.clone())); let font = path!("f.ttf").map(|| FONT_FILE); let listing = { let rooms = rooms.clone(); let pubs = public_rooms.clone(); path!("rlist").and_then(move || { let rooms = rooms.clone(); let pubs = pubs.clone(); async move { let roomsl = rooms.read().await; let pubsl = pubs.read().await; let rooms_pcount = futures::stream::iter(pubsl.iter()) .then(|(id, _):(&RoomId,_)| { let roomsl = roomsl.clone(); async move { (id.clone(), roomsl.get(id).unwrap().read().await.players.read().await.len()) } }) .collect::>().await; let resp = (&*pubsl, rooms_pcount); Ok::<_,std::convert::Infallible>( reply::json(&resp) ) } }) }; let roomspace = { let rooms = rooms.clone(); path!("rspace").and_then(move || { let r = rooms.clone(); async move { let empty_len = empty_rooms(r.clone()).await.len(); let space = ROOM_LIMIT - r.read().await.len() + empty_len; Ok::<_,std::convert::Infallible>( hyper::Response::builder() .status(hyper::StatusCode::OK) .body(hyper::Body::from(space.to_string())) .unwrap() ) } }) }; let rform_recv = { let rooms = rooms.clone(); let pubs = public_rooms.clone(); post().and(path("r")).and(body::content_length_limit(4096)).and(body::form()) .and_then(move |rinfo: HashMap| { println!("{:?}", rinfo); let rooms = rooms.clone(); let pubs = pubs.clone(); async move { if let (Some(w),Some(h),Some(num),Some(denom),access,asfm,limit) = ( rinfo.get("rwidth").and_then(|wt| wt.parse::().ok()), rinfo.get("rheight").and_then(|ht| ht.parse::().ok()), rinfo.get("rration").and_then(|nt| nt.parse::().ok()), rinfo.get("rratiod").and_then(|dt| dt.parse::().ok()), rinfo.get("raccess"), rinfo.get("ralwayssafe1move"), rinfo.get("rlimit").and_then(|l| l.parse::().ok()), ) { if w.get()*h.get() > AREA_LIMIT { return Err(reject::custom(BoardTooBig)) } let board_conf = minesweeper::BoardConf { w, h, mine_ratio: (num,denom), always_safe_first_move: asfm.is_some() }; let mut rooms = rooms.write().await; let uid = types::RoomId::new_in(&rooms); let name = { let n = rinfo.get("rname").map(|r| r.to_owned()).unwrap(); if n.is_empty() { uid.to_string() } else { n } }; let players = PlayerMap::default(); let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel(); let handle = tokio::spawn(gameloop(cmd_rx, players.clone(), board_conf)); let room_conf = RoomConf { name, player_cap: match limit { Some(i) => i, None => usize::MAX }, public: access.is_some(), board_conf, }; let new_room = Room { conf: room_conf, players, driver: handle, cmd_stream: cmd_tx, }; if access.is_some() { pubs.write().await.insert(uid.clone(), serde_json::to_string(&new_room.conf).unwrap()); } rooms.insert(uid.clone(), Arc::new(RwLock::new(new_room))); Ok( hyper::Response::builder() .status(hyper::StatusCode::SEE_OTHER) .header(hyper::header::LOCATION, format!("/room/{uid}")) .body(hyper::Body::empty()) .unwrap() ) } else { Err(reject::custom(BadFormData)) } } }) }; let room = { let rooms_ws = rooms.clone(); let rooms_lobby = rooms.clone(); let prefix = get().and(path!("room" / String / ..)); // Fixme: better errors prefix.and(path!("ws")) .and(ws()) .and(addr::remote()) .and_then(move |id: String, websocket: warp::ws::Ws, saddr: Option| { let rooms = rooms_ws.clone(); async move { let id = RoomId {0: id}; match rooms.read().await.get(&id).map(|x| x.clone()) { Some(r) => { println!("conn from {saddr:?} into {id}"); Ok(websocket.on_upgrade(move |socket| { conn::lobby(socket, saddr.expect("socket without address"), r.clone()) })) }, None => { println!("conn from {saddr:?} into inexistent room {id}"); Err(reject()) } } } }) .or(prefix.and(path::end()) .and(fs::file(conf.room_pg.clone())) .then(move |id: String, f: fs::File| { let rooms = rooms_lobby.clone(); async move { if rooms.read().await.contains_key(&RoomId {0: id}) { f.into_response() } else { reply::with_status("No such room", http::StatusCode::BAD_REQUEST).into_response() } } }) ) }; let route = get() .and(index) .or(style) .or(code) .or(font) .or(listing) .or(roomspace) .or(rform_recv) .or(room) .recover(error_handler); let server = warp::serve(route) .tls() .cert_path(conf.cert) .key_path(conf.pkey) .run(conf.socket_addr); println!("Serving on {}", conf.socket_addr); server.await; Ok(()) } // If a move is made, broadcast new board, else just send current board async fn gameloop(mut move_rx: tokio::sync::mpsc::UnboundedReceiver, players: PlayerMapData, bconf: minesweeper::BoardConf) { use minesweeper::*; use flate2::{ Compression, write::DeflateEncoder }; use std::io::Write; let mut game = Game::new(bconf); let mut latest_player_name = None; while let Some(req) = move_rx.recv().await { let done = game.phase == Phase::Die || game.phase == Phase::Win; match req { MetaMove::Move(m, o) => if !done { game = game.act(m); if game.phase == Phase::Win || game.phase == Phase::Die { game.board = game.board.grade(); } latest_player_name = players.read().await.get(&o).map(|p| p.name.clone()); }, MetaMove::Dump => (), MetaMove::Reset => { game = Game::new(bconf); }, } use warp::ws::Message; let mut board_encoder = DeflateEncoder::new(Vec::new(), Compression::default()); board_encoder.write_all(&game.board.render()).unwrap(); let compressed_board = board_encoder.finish().unwrap(); let mut reply = vec![Message::binary(compressed_board)]; let lpname = latest_player_name.as_ref().map(|s| s.as_str()).unwrap_or("unknown player"); match game.phase { Phase::Win => { reply.push(Message::text(format!("win {lpname}"))); }, Phase::Die => { reply.push(Message::text(format!("lose {lpname}"))); }, _ => (), } { let peers = players.read().await; for (addr, p) in peers.iter() { for r in reply.iter() { if let Err(e) = p.conn.tx.send(r.clone()) { println!("couldn't send game update {r:?} to {addr}: {e}"); } } } } } } use warp::{ reject::{ Reject, Rejection }, reply::{ self, Reply }, http::StatusCode }; #[derive(Debug)] struct BadFormData; impl Reject for BadFormData {} #[derive(Debug)] struct BoardTooBig; impl Reject for BoardTooBig {} async fn error_handler(err: Rejection) -> Result { if err.is_not_found() { Ok(reply::with_status("No such file", StatusCode::NOT_FOUND)) } else if let Some(_e) = err.find::() { Ok(reply::with_status("Bad form data", StatusCode::BAD_REQUEST)) } else if let Some(_e) = err.find::() { Ok(reply::with_status("Board too big", StatusCode::BAD_REQUEST)) } else { println!("unhandled rejection: {err:?}"); Ok(reply::with_status("Server error", StatusCode::INTERNAL_SERVER_ERROR)) } } async fn empty_rooms(rooms: RoomMap) -> Vec { let rl = rooms.read().await; futures::stream::iter(rl.iter()) .filter_map(|(id,roomarc)| async move { let rrl = roomarc.read().await; let rrrl = rrl.players.read().await; if rrrl.len() == 0 { Some(id.clone()) } else { None } }) .collect::>().await }