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 { | 
