119 lines
3.3 KiB
Rust
119 lines
3.3 KiB
Rust
#[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<impl Reply, Rejection> {
|
|
serve_file("index.html")
|
|
}
|
|
|
|
async fn serve_asset(path: Tail) -> Result<impl Reply, Rejection> {
|
|
serve_file(path.as_str())
|
|
}
|
|
|
|
fn serve_file(path: &str) -> Result<impl Reply, Rejection> {
|
|
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::<ClientMessage>(json)
|
|
.context("Parsing JSON")
|
|
.map(Some),
|
|
Err(()) => {
|
|
debug!("Non-text message {:?}", &msg);
|
|
Ok(None)
|
|
}
|
|
})
|
|
});
|
|
|
|
ClientAgent::new(game_server).run(&mut websocket).await
|
|
}
|