mod client; mod communication; mod connection; mod game; mod host; mod lobby; mod runner; mod saver; use axum::{ Router, http::{Request, header}, response::IntoResponse, routing::{any, get}, }; use axum_extra::headers; use communication::lobby::LobbyComms; use connection::JoinedPlayers; use core::{fmt::Display, net::SocketAddr, str::FromStr}; use log::Record; use runner::IdentifiedClientMessage; use std::{env, io::Write, path::Path}; use tokio::{ sync::{broadcast, mpsc}, time::Instant, }; use crate::{ communication::{Comms, host::HostComms, player::PlayerIdComms}, saver::FileSaver, }; const DEFAULT_PORT: u16 = 8080; const DEFAULT_HOST: &str = "127.0.0.1"; const DEFAULT_SAVE_DIR: &str = "werewolves-saves/"; #[tokio::main] async fn main() { // pretty_env_logger::init(); use colored::Colorize; pretty_env_logger::formatted_builder() .parse_default_env() .format(|f, record| { let time = chrono::Local::now().time().to_string().dimmed(); match record.file() { Some(file) => { let file = format!( "[{file}{}]", record .line() .map(|l| format!(":{l}")) .unwrap_or_else(String::new), ) .dimmed(); let level = match record.level() { log::Level::Error => "[err]".red().bold(), log::Level::Warn => "[warn]".yellow().bold(), log::Level::Info => "[info]".white().bold(), log::Level::Debug => "[debug]".dimmed().bold(), log::Level::Trace => "[trace]".dimmed(), }; let args = record.args(); let arrow = "➢".bold().magenta(); writeln!( f, "{time} {file}\n{level} {arrow} {args}", // "⇗⇘⇗⇘⇗⇘".bold().dimmed(), ) } _ => writeln!(f, "{time} [{}] {}", record.level(), record.args()), } }) .try_init() .unwrap(); let default_panic = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { default_panic(info); std::process::exit(1); })); let host = env::var("HOST").unwrap_or(DEFAULT_HOST.to_string()); let port = env::var("PORT") .map_err(|err| anyhow::anyhow!("{err}")) .map(|port_str| { port_str .parse::() .unwrap_or_else(|err| panic!("parse PORT={port_str} failed: {err}")) }) .unwrap_or(DEFAULT_PORT); let listen_addr = SocketAddr::from_str(format!("{host}:{port}").as_str()).expect("invalid host/port"); let (send, recv) = broadcast::channel(100); let (server_send, host_recv) = broadcast::channel(100); let (host_send, server_recv) = mpsc::channel(100); let (connect_send, connect_recv) = broadcast::channel(100); let joined_players = JoinedPlayers::new(connect_send); let lobby_comms = LobbyComms::new( Comms::new( HostComms::new(server_send, server_recv), PlayerIdComms::new(joined_players.clone(), recv, connect_recv.resubscribe()), ), connect_recv, ); let jp_clone = joined_players.clone(); let path = Path::new(option_env!("SAVE_PATH").unwrap_or(DEFAULT_SAVE_DIR)) .canonicalize() .expect("canonicalizing path"); if let Err(err) = std::fs::create_dir(&path) && !matches!(err.kind(), std::io::ErrorKind::AlreadyExists) { panic!("creating save dir at [{path:?}]: {err}") } // Check if we can write to the path { let test_file_path = path.join(".test"); if let Err(err) = std::fs::File::create(&test_file_path) { panic!("can't create files in {path:?}: {err}") } std::fs::remove_file(&test_file_path).log_err(); } let saver = FileSaver::new(path); tokio::spawn(async move { crate::runner::run_game(jp_clone, lobby_comms, saver).await; panic!("game over"); }); let state = AppState { joined_players, host_recv, host_send, send, }; let app = Router::new() .route("/connect/client", any(client::handler)) .route("/connect/host", any(host::handler)) .with_state(state) .fallback(get(handle_http_static)); let listener = tokio::net::TcpListener::bind(listen_addr).await.unwrap(); log::info!("listening on {}", listener.local_addr().unwrap()); axum::serve( listener, app.into_make_service_with_connect_info::(), ) .await .unwrap(); } struct AppState { joined_players: JoinedPlayers, send: broadcast::Sender, host_send: tokio::sync::mpsc::Sender, host_recv: broadcast::Receiver, } impl Clone for AppState { fn clone(&self) -> Self { Self { joined_players: self.joined_players.clone(), send: self.send.clone(), host_send: self.host_send.clone(), host_recv: self.host_recv.resubscribe(), } } } async fn handle_http_static(req: Request) -> impl IntoResponse { use mime_sniffer::MimeTypeSniffer; const INDEX_FILE: &[u8] = include_bytes!("../../werewolves/dist/index.html"); let path = req.uri().path(); werewolves_macros::include_dist!(DIST_FILES, "werewolves/dist"); let file = if let Some(file) = DIST_FILES.iter().find_map(|(file_path, file)| { if *file_path == path { Some(*file) } else { None } }) { file } else { return ( [(header::CONTENT_TYPE, "text/html".to_string())], INDEX_FILE, ); }; let mime = if path.ends_with(".js") { "text/javascript".to_string() } else if path.ends_with(".css") { "text/css".to_string() } else if path.ends_with(".wasm") { "application/wasm".to_string() } else { file.sniff_mime_type() .unwrap_or("application/octet-stream") .to_string() }; ([(header::CONTENT_TYPE, mime)], file) } struct XForwardedFor(String); impl Display for XForwardedFor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.0.as_str()) } } impl headers::Header for XForwardedFor { fn name() -> &'static header::HeaderName { static NAME: header::HeaderName = header::HeaderName::from_static("x-forwarded-for"); &NAME } fn decode<'i, I>(values: &mut I) -> Result where Self: Sized, I: Iterator, { Ok(Self( values .next() .and_then(|v| v.to_str().ok()) .ok_or(headers::Error::invalid())? .to_string(), )) } fn encode>(&self, _: &mut E) {} } pub trait LogError { fn log_warn(self); fn log_err(self); fn log_debug(self); } impl LogError for Result where E: Display, { fn log_warn(self) { if let Err(err) = self { log::warn!("{err}"); } } fn log_err(self) { if let Err(err) = self { log::error!("{err}"); } } fn log_debug(self) { if let Err(err) = self { log::debug!("{err}") } } }