diff options
author | stale <redkugelblitzin@gmail.com> | 2022-05-20 06:39:58 -0300 |
---|---|---|
committer | stale <redkugelblitzin@gmail.com> | 2022-05-20 06:39:58 -0300 |
commit | fae0fca7aabb81325a296a1d6202239c3db44b60 (patch) | |
tree | 687a95f381ee13ee3de9f8eabdf4cd371a258091 | |
parent | d89b9ea43ac65af549279f6e86c68c66243dcdf3 (diff) |
wip room support
-rw-r--r-- | assets/client.js | 10 | ||||
-rw-r--r-- | assets/index.html | 28 | ||||
-rw-r--r-- | assets/room.html | 6 | ||||
-rw-r--r-- | assets/style.css | 5 | ||||
-rw-r--r-- | src/main.rs | 160 | ||||
-rw-r--r-- | src/types.rs | 27 |
6 files changed, 188 insertions, 48 deletions
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 =` - <input id="name-in" type="text" value="anon"> - <input id="clr-in" type="color" value="#33c033"></input> - <button onclick=register()>Join</button>`; + <form action="javascript:;" onsubmit="register()"> + <input id="name-in" type="text" value="anon"> + <input id="clr-in" type="color" value="#33c033"></input> + <button>Join</button> + </form>`; 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 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <title>websweeper</title> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <link rel="stylesheet" type="text/css" href="s.css"> + </head> + <body> + <form method="post" action="r"> + <fieldset> + <legend>-={ Create a new room }=-</legend> + <label>room name<input name="rname" type="text" autofocus></label><br> + <label>board dimensions<br> + <input name="rwidth" type="number" value="30" required> + x + <input name="rheight" type="number" value="20" required><br> + where<input name="rration" type="number" value="1" required> + in every<input name="rratiod" type="number" value="8" required> + tiles are mines + </label><br> + <label>public, ie. shown in the lobby? <input name="raccess" type="checkbox"></label><br> + <label>player limit (0 for none)<input name="rlimit" type="number"></label><br> + <button>create</button> + </fieldset> + <form> + </body> +</html> 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 @@ <meta charset="UTF-8"> <title>websweeper</title> <meta name="viewport" content="width=device-width,initial-scale=1"> - <link rel="stylesheet" type="text/css" href="s.css"> + <link rel="stylesheet" type="text/css" href="/s.css"> </head> <body> - <div class=""> + <div> <div id="board-container"> <span id="board"></span> </div> <p id="miscinfo">Loading...</p> </div> </body> - <script src="c.js"></script> + <script src="/c.js"></script> </html> 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<dyn Error>> { 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<dyn Error>> { #[tokio::main] async fn tokio_main(conf: Config) -> Result<(), Box<dyn Error>> { - // 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<String, String>| { + 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::<usize>().ok()), + rinfo.get("rheight").and_then(|ht| ht.parse::<usize>().ok()), + rinfo.get("rration").and_then(|nt| nt.parse::<usize>().ok()), + rinfo.get("rratiod").and_then(|dt| dt.parse::<usize>().ok()), + rinfo.get("raccess"), + rinfo.get("rlimit").and_then(|l| l.parse::<usize>().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<SocketAddr>| { - 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<SocketAddr>| { + 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<MetaMove>, 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<impl Reply, std::convert::Infallible> { + if err.is_not_found() { Ok(reply::with_status("No such file", StatusCode::NOT_FOUND)) } + else if let Some(_e) = err.find::<BadFormData>() { + 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<T>(map: &HashMap<RoomId, T>) -> Self { + use rand::{ thread_rng, Rng, distributions::Alphanumeric }; + let id = RoomId { 0: thread_rng() + .sample_iter(&Alphanumeric) + .take(16) + .map(char::from) + .collect::<String>() }; + if map.contains_key(&id) { RoomId::new_in(map) } + else { id } + } +} + pub type CmdTx = tokio::sync::mpsc::UnboundedSender<MetaMove>; +pub type RoomMap = Arc<RwLock<HashMap<RoomId, Arc<RwLock<Room>>>>>; pub type PlayerMapData = Arc<RwLock<HashMap<SocketAddr, Player>>>; #[derive(Debug)] pub struct PlayerMap { |