summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorstale <redkugelblitzin@gmail.com>2022-05-20 06:39:58 -0300
committerstale <redkugelblitzin@gmail.com>2022-05-20 06:39:58 -0300
commitfae0fca7aabb81325a296a1d6202239c3db44b60 (patch)
tree687a95f381ee13ee3de9f8eabdf4cd371a258091
parentd89b9ea43ac65af549279f6e86c68c66243dcdf3 (diff)
wip room support
-rw-r--r--assets/client.js10
-rw-r--r--assets/index.html28
-rw-r--r--assets/room.html6
-rw-r--r--assets/style.css5
-rw-r--r--src/main.rs160
-rw-r--r--src/types.rs27
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 {