From fae0fca7aabb81325a296a1d6202239c3db44b60 Mon Sep 17 00:00:00 2001 From: stale Date: Fri, 20 May 2022 06:39:58 -0300 Subject: wip room support --- assets/client.js | 10 ++-- assets/index.html | 28 ++++++++++ assets/room.html | 6 +- assets/style.css | 5 +- src/main.rs | 160 +++++++++++++++++++++++++++++++++++++++++------------- src/types.rs | 27 ++++++++- 6 files changed, 188 insertions(+), 48 deletions(-) create mode 100644 assets/index.html diff --git a/assets/client.js b/assets/client.js index 0e90d4b..f888104 100644 --- a/assets/client.js +++ b/assets/client.js @@ -3,9 +3,11 @@ window.name = "anon"; window.clr = "#f03333"; window.info_elem = document.getElementById("miscinfo"); window.info_elem.innerHTML =` - - - `; +
+ + + +
`; window.board_elem = document.getElementById("board"); window.bwidth = NaN; window.bheight = NaN; @@ -25,7 +27,7 @@ function register() { function connect() { let wsproto = (window.location.protocol == "https:")? "wss:": "ws:"; - let s = new WebSocket(`${wsproto}//${window.location.hostname}:${window.location.port}${window.location.pathname}ws`); + let s = new WebSocket(`${wsproto}//${location.hostname}:${location.port}${location.pathname}/ws`); s.onopen = function() { s.send(`register ${window.name} ${window.clr}`); } diff --git a/assets/index.html b/assets/index.html new file mode 100644 index 0000000..bb18d1c --- /dev/null +++ b/assets/index.html @@ -0,0 +1,28 @@ + + + + + websweeper + + + + +
+
+ -={ Create a new room }=- +
+
+
+
+ +
+ + + diff --git a/assets/room.html b/assets/room.html index f3f130d..e3e6ff9 100644 --- a/assets/room.html +++ b/assets/room.html @@ -4,15 +4,15 @@ websweeper - + -
+

Loading...

- + diff --git a/assets/style.css b/assets/style.css index acdd9c0..a8936e6 100644 --- a/assets/style.css +++ b/assets/style.css @@ -1,6 +1,6 @@ @font-face { font-family: vt323; - src: url("f.ttf"); + src: url("/f.ttf"); } #board-container { font-size: 36px; @@ -11,6 +11,9 @@ body { background-color: black; color: white; } +form { + margin: 0 auto; +} .unsel { -webkit-touch-callout: none; -webkit-user-select: none; diff --git a/src/main.rs b/src/main.rs index e972025..6c2f375 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use std::{ error::Error, net::SocketAddr, sync::Arc, + collections::HashMap, }; mod types; @@ -17,8 +18,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(), - form_pg: "./assets/form.html".to_owned(), client_code: "./assets/client.js".to_owned(), stylesheet: "./assets/style.css".to_owned(), socket_addr: ([0,0,0,0],31235).into(), @@ -29,50 +30,117 @@ fn main() -> Result<(), Box> { #[tokio::main] async fn tokio_main(conf: Config) -> Result<(), Box> { - // Start the temporary single room - let room = Arc::new(RwLock::new({ - let name = "Testing room".to_string(); - let players = PlayerMap::default(); - let bconf = BoardConf { w: 75, h: 35, mine_ratio: (1, 8) }; - let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel(); - let handle = tokio::spawn(gameloop(cmd_rx, players.clone(), bconf)); - Room { - name, - players, - peer_limit: 32, - board_conf: bconf, - cmd_stream: cmd_tx, - driver: handle, - } - })); - + let rooms: RoomMap = Arc::new(RwLock::new(HashMap::new())); + let public_rooms = Arc::new(RwLock::new(Vec::new())); use warp::*; - 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 = path("rlist").map(|| "placeholder'em"); - let room_form = path("r").map(|| "yeah placeholder mate"); - let index = path::end().and(fs::file(conf.room_pg.clone())); - - let websocket_route = { - let room = room.clone(); - use warp::*; - path("ws") + 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 = path!("rlist").map(|| "placeholder'em"); + 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,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("rlimit").and_then(|l| l.parse::().ok()), + ) { + let board_conf = BoardConf { w, h, mine_ratio: (num,denom) }; + let name = rinfo.get("rname").map(|r| r.to_owned()).unwrap_or(format!("{w}x{h} room")); + + let mut rooms = rooms.write().await; + let uid = types::RoomId::new_in(&rooms); + 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)); + rooms.insert(uid.clone(), Arc::new(RwLock::new(Room { + name, + players, + peer_limit: match limit { Some(i) => i, None => usize::MAX }, + public: access.is_some(), + driver: handle, + cmd_stream: cmd_tx, + board_conf, + }))); + if access.is_some() { + pubs.write().await.push(uid.clone()); + } + 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()) - .map(move |ws: warp::ws::Ws, saddr: Option| { - let room = room.clone(); - println!("conn from {saddr:?}"); - ws.on_upgrade(move |socket| { - conn::lobby(socket, saddr.expect("socket without address"), room.clone()) - }) + .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 = any().and(get().and(index).or(style).or(code).or(font).or(listing)).or(post().and(room_form)); - let routes = websocket_route.or(route); - let server = warp::serve(routes) + + 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) @@ -121,3 +189,19 @@ async fn gameloop(mut move_rx: tokio::sync::mpsc::UnboundedReceiver, p } } } + +use warp::{ reject::{ Reject, Rejection }, reply::{ self, Reply }, http::StatusCode }; +#[derive(Debug)] +struct BadFormData; +impl Reject for BadFormData {} + +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 { + println!("unhandled rejection: {err:?}"); + Ok(reply::with_status("Server error", StatusCode::INTERNAL_SERVER_ERROR)) + } +} + diff --git a/src/types.rs b/src/types.rs index fb9f7ae..df9e168 100644 --- a/src/types.rs +++ b/src/types.rs @@ -16,8 +16,8 @@ use crate::minesweeper; pub struct Config { pub cert: String, pub pkey: String, + pub index_pg: String, pub room_pg: String, - pub form_pg: String, pub client_code: String, pub stylesheet: String, pub socket_addr: SocketAddr, @@ -41,7 +41,8 @@ impl Display for BoardConf { pub struct Room { pub name: String, pub players: PlayerMap, - pub peer_limit: u32, + pub peer_limit: usize, + pub public: bool, pub driver: tokio::task::JoinHandle<()>, pub cmd_stream: CmdTx, pub board_conf: BoardConf, @@ -75,7 +76,29 @@ impl Display for Player { } } +#[derive(Eq, PartialEq, Hash, Debug, Clone)] +pub struct RoomId(pub String); +impl Display for RoomId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl RoomId { + pub fn new_in(map: &HashMap) -> Self { + use rand::{ thread_rng, Rng, distributions::Alphanumeric }; + let id = RoomId { 0: thread_rng() + .sample_iter(&Alphanumeric) + .take(16) + .map(char::from) + .collect::() }; + if map.contains_key(&id) { RoomId::new_in(map) } + else { id } + } +} + pub type CmdTx = tokio::sync::mpsc::UnboundedSender; +pub type RoomMap = Arc>>>>; pub type PlayerMapData = Arc>>; #[derive(Debug)] pub struct PlayerMap { -- cgit v1.2.3