#[macro_use] extern crate log; use anyhow::{Context, Result}; use future::ready; use futures::prelude::*; use rust_embed::RustEmbed; use serde_json::{from_str, to_string}; use std::net::ToSocketAddrs; use stream::FuturesUnordered; use structopt::StructOpt; use warp::{ http::HeaderValue, path, reject, reply::Response, serve, ws, ws::Ws, Filter, Rejection, Reply, }; use ws::{Message, WebSocket}; use path::Tail; use net::{ agent::ClientAgent, server::{Handle, Server}, ClientMessage, ServerMessage, }; pub mod net; #[derive(StructOpt)] /// Server for base2020 lockstep protocol for multiplayer games. struct Args { /// The socket address to listen for connections on; /// can be a hostname to bind to multiple hosts at once, /// such as to listen on both IPv4 & IPv6. listen: String, } #[derive(RustEmbed)] #[folder = "dist/"] struct Assets; #[tokio::main] async fn main() -> Result<()> { env_logger::init(); let args = Args::from_args(); // create singleton server (use weak-table in the future for multiple rooms) let game_server = Server::create(); // dispatch websockets let socket_handler = ws().map(move |upgrade: Ws| { let handle = game_server.clone(); upgrade.on_upgrade(move |ws| { async { if let Err(error) = handle_socket(handle, ws).await { warn!("Websocket connection lost: {:#}", error); } } }) }); // assemble routes let routes = path!("base2020.ws").and(socket_handler) .or(path::end().and_then(serve_index)) .or(path::tail().and_then(serve_asset)) ; let addrs = args .listen .to_socket_addrs() .context("Couldn't parse the listen address")?; let servers = FuturesUnordered::new(); for addr in addrs { let (_, server) = serve(routes.clone()).try_bind_ephemeral(addr)?; servers.push(server); } servers.for_each(|_| async {}).await; Ok(()) } async fn serve_index() -> Result { serve_file("index.html") } async fn serve_asset(path: Tail) -> Result { serve_file(path.as_str()) } fn serve_file(path: &str) -> Result { let asset = Assets::get(path).ok_or_else(reject::not_found)?; let mime_type = mime_guess::from_path(path).first_or_octet_stream(); let mut response = Response::new(asset.into()); let type_header = HeaderValue::from_str(mime_type.as_ref()).unwrap(); response.headers_mut().insert("content-type", type_header); Ok(response) } async fn handle_socket(game_server: Handle, websocket: WebSocket) -> Result<()> { let mut websocket = websocket.with(|msg: ServerMessage| { ready( to_string(&msg) .context("JSON encoding shouldn't fail") .map(|json| Message::text(json)), ) }).map_err(Into::into).try_filter_map(|msg| { ready(match msg.to_str() { Ok(json) => from_str::(json) .context("Parsing JSON") .map(Some), Err(()) => { debug!("Non-text message {:?}", &msg); Ok(None) } }) }); ClientAgent::new(game_server).run(&mut websocket).await }