use std::{ error::Error, net::SocketAddr, sync::Arc, collections::HashMap, num::NonZeroUsize, }; 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; 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 pubs = public_rooms.clone(); path!("rlist").and_then(move || { let pubs = pubs.clone(); async move { let map = pubs.read().await; Ok::<_,std::convert::Infallible>( reply::json(&*map) ) }}) }; 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(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::*; 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 reply = vec![Message::binary(game.board.render())]; 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)) } }