diff --git a/minigolf_client/src/main.rs b/minigolf_client/src/main.rs index 248343b..4c915e2 100644 --- a/minigolf_client/src/main.rs +++ b/minigolf_client/src/main.rs @@ -1,26 +1,17 @@ +mod network; +mod ui; + use { - aeronet::io::{ - Session, SessionEndpoint, - connection::{Disconnect, DisconnectReason, Disconnected}, - }, - aeronet_replicon::client::{AeronetRepliconClient, AeronetRepliconClientPlugin}, - aeronet_websocket::client::{WebSocketClient, WebSocketClientPlugin}, - aeronet_webtransport::{ - cert, - client::{WebTransportClient, WebTransportClientPlugin}, - }, + crate::{network::ClientNetworkPlugin, ui::ClientUiPlugin}, + aeronet::io::{Session, connection::Disconnected}, bevy::{ color::palettes::basic::RED, - ecs::query::QuerySingleError, input::{ - common_conditions::{input_just_released, input_pressed, input_toggle_active}, + common_conditions::{input_just_released, input_pressed}, mouse::{MouseMotion, MouseWheel}, }, prelude::*, }, - bevy_egui::{EguiContexts, EguiPlugin, egui}, - bevy_inspector_egui::quick::WorldInspectorPlugin, - bevy_replicon::prelude::*, minigolf::{GameState, LevelMesh, MinigolfPlugin, Player, PlayerInput}, web_sys::{HtmlCanvasElement, wasm_bindgen::JsCast}, }; @@ -28,28 +19,15 @@ use { fn main() -> AppExit { App::new() .add_plugins(( - // core DefaultPlugins, - EguiPlugin, - WorldInspectorPlugin::default().run_if(input_toggle_active(true, KeyCode::Escape)), - // transport - WebTransportClientPlugin, - WebSocketClientPlugin, - // SessionVisualizerPlugin, - // replication - RepliconPlugins, - AeronetRepliconClientPlugin, - // game + ClientUiPlugin, + ClientNetworkPlugin, MinigolfPlugin, )) - .init_resource::() - .init_resource::() - .init_resource::() .add_systems(Startup, setup_level) .add_systems( Update, ( - (web_transport_ui, web_socket_ui, global_ui).chain(), handle_inputs.run_if(in_state(GameState::Playing)), launch_inputs .run_if(in_state(GameState::Playing).and(input_pressed(MouseButton::Left))), @@ -64,7 +42,6 @@ fn main() -> AppExit { draw_gizmos, ), ) - .add_observer(on_connecting) .add_observer(on_connected) .add_observer(on_player_added) .add_observer(on_level_mesh_added) @@ -72,23 +49,6 @@ fn main() -> AppExit { .run() } -#[derive(Debug, Default, Resource)] -struct GlobalUi { - session_id: usize, - log: Vec, -} - -#[derive(Debug, Default, Resource)] -struct WebTransportUi { - target: String, - cert_hash: String, -} - -#[derive(Debug, Default, Resource)] -struct WebSocketUi { - target: String, -} - #[derive(Debug, Clone, Component, Deref, DerefMut, Reflect)] struct AccumulatedInputs { input: Vec2, @@ -123,18 +83,6 @@ fn setup_level(mut commands: Commands) { )); } -fn on_connecting( - trigger: Trigger, - names: Query<&Name>, - mut ui_state: ResMut, -) { - let entity = trigger.entity(); - let name = names - .get(entity) - .expect("our session entity should have a name"); - ui_state.log.push(format!("{name} connecting")); -} - fn on_player_added( trigger: Trigger, server: Res, @@ -178,268 +126,14 @@ fn on_level_mesh_added( )); } -fn on_connected( - trigger: Trigger, - names: Query<&Name>, - mut ui_state: ResMut, - mut game_state: ResMut>, -) { - let entity = trigger.entity(); - let name = names - .get(entity) - .expect("our session entity should have a name"); - ui_state.log.push(format!("{name} connected")); - +fn on_connected(_trigger: Trigger, mut game_state: ResMut>) { game_state.set(GameState::Playing); } -fn on_disconnected( - trigger: Trigger, - names: Query<&Name>, - mut ui_state: ResMut, - mut game_state: ResMut>, -) { - let session = trigger.entity(); - let name = names - .get(session) - .expect("our session entity should have a name"); - ui_state.log.push(match &trigger.reason { - DisconnectReason::User(reason) => { - format!("{name} disconnected by user: {reason}") - } - DisconnectReason::Peer(reason) => { - format!("{name} disconnected by peer: {reason}") - } - DisconnectReason::Error(err) => { - format!("{name} disconnected due to error: {err:?}") - } - }); +fn on_disconnected(_trigger: Trigger, mut game_state: ResMut>) { game_state.set(GameState::None); } -fn global_ui( - mut commands: Commands, - mut egui: EguiContexts, - global_ui: Res, - sessions: Query<(Entity, &Name, Option<&Session>), With>, - replicon_client: Res, -) { - let stats = replicon_client.stats(); - egui::Window::new("Session Log").show(egui.ctx_mut(), |ui| { - ui.label("Replicon reports:"); - ui.horizontal(|ui| { - ui.label(match replicon_client.status() { - RepliconClientStatus::Disconnected => "Disconnected", - RepliconClientStatus::Connecting => "Connecting", - RepliconClientStatus::Connected { .. } => "Connected", - }); - ui.separator(); - - ui.label(format!("RTT {:.0}ms", stats.rtt * 1000.0)); - ui.separator(); - - ui.label(format!("Pkt Loss {:.1}%", stats.packet_loss * 100.0)); - ui.separator(); - - ui.label(format!("Rx {:.0}bps", stats.received_bps)); - ui.separator(); - - ui.label(format!("Tx {:.0}bps", stats.sent_bps)); - }); - match sessions.get_single() { - Ok((session, name, connected)) => { - if connected.is_some() { - ui.label(format!("{name} connected")); - } else { - ui.label(format!("{name} connecting")); - } - - if ui.button("Disconnect").clicked() { - commands.trigger_targets(Disconnect::new("disconnected by user"), session); - } - } - Err(QuerySingleError::NoEntities(_)) => { - ui.label("No sessions active"); - } - Err(QuerySingleError::MultipleEntities(_)) => { - ui.label("Multiple sessions active"); - } - } - - ui.separator(); - - for msg in &global_ui.log { - ui.label(msg); - } - }); -} - -// -// WebTransport -// - -fn web_transport_ui( - mut commands: Commands, - mut egui: EguiContexts, - mut global_ui: ResMut, - mut ui_state: ResMut, - sessions: Query<(), With>, -) { - const DEFAULT_TARGET: &str = "https://remote-dev:25565"; - - egui::Window::new("WebTransport").show(egui.ctx_mut(), |ui| { - if sessions.iter().next().is_some() { - ui.disable(); - } - - let enter_pressed = ui.input(|i| i.key_pressed(egui::Key::Enter)); - - let mut connect = false; - ui.horizontal(|ui| { - let connect_resp = ui.add( - egui::TextEdit::singleline(&mut ui_state.target) - .hint_text(format!("{DEFAULT_TARGET} | [enter] to connect")), - ); - connect |= connect_resp.lost_focus() && enter_pressed; - connect |= ui.button("Connect").clicked(); - }); - - let cert_hash_resp = ui.add( - egui::TextEdit::singleline(&mut ui_state.cert_hash) - .hint_text("(optional) certificate hash"), - ); - connect |= cert_hash_resp.lost_focus() && enter_pressed; - - if connect { - let mut target = ui_state.target.clone(); - if target.is_empty() { - DEFAULT_TARGET.clone_into(&mut target); - } - - let cert_hash = ui_state.cert_hash.clone(); - let config = web_transport_config(cert_hash); - - global_ui.session_id += 1; - let name = format!("{}. {target}", global_ui.session_id); - commands - .spawn((Name::new(name), AeronetRepliconClient)) - .queue(WebTransportClient::connect(config, target)); - } - }); -} - -type WebTransportClientConfig = aeronet_webtransport::client::ClientConfig; - -#[cfg(target_family = "wasm")] -fn web_transport_config(cert_hash: String) -> WebTransportClientConfig { - use aeronet_webtransport::xwt_web::{CertificateHash, HashAlgorithm}; - - let server_certificate_hashes = match cert::hash_from_b64(&cert_hash) { - Ok(hash) => vec![CertificateHash { - algorithm: HashAlgorithm::Sha256, - value: Vec::from(hash), - }], - Err(err) => { - warn!("Failed to read certificate hash from string: {err:?}"); - Vec::new() - } - }; - - WebTransportClientConfig { - server_certificate_hashes, - ..Default::default() - } -} - -#[cfg(not(target_family = "wasm"))] -fn web_transport_config(cert_hash: String) -> WebTransportClientConfig { - use {aeronet_webtransport::wtransport::tls::Sha256Digest, core::time::Duration}; - - let config = WebTransportClientConfig::builder().with_bind_default(); - - let config = if cert_hash.is_empty() { - warn!("Connecting without certificate validation"); - config.with_no_cert_validation() - } else { - match cert::hash_from_b64(&cert_hash) { - Ok(hash) => config.with_server_certificate_hashes([Sha256Digest::new(hash)]), - Err(err) => { - warn!("Failed to read certificate hash from string: {err:?}"); - config.with_server_certificate_hashes([]) - } - } - }; - - config - .keep_alive_interval(Some(Duration::from_secs(1))) - .max_idle_timeout(Some(Duration::from_secs(5))) - .expect("should be a valid idle timeout") - .build() -} - -// -// WebSocket -// - -fn web_socket_ui( - mut commands: Commands, - mut egui: EguiContexts, - mut global_ui: ResMut, - mut ui_state: ResMut, - sessions: Query<(), With>, -) { - const DEFAULT_TARGET: &str = "ws://remote-dev:25566"; - - egui::Window::new("WebSocket").show(egui.ctx_mut(), |ui| { - if sessions.iter().next().is_some() { - ui.disable(); - } - - let enter_pressed = ui.input(|i| i.key_pressed(egui::Key::Enter)); - - let mut connect = false; - ui.horizontal(|ui| { - let connect_resp = ui.add( - egui::TextEdit::singleline(&mut ui_state.target) - .hint_text(format!("{DEFAULT_TARGET} | [enter] to connect")), - ); - connect |= connect_resp.lost_focus() && enter_pressed; - connect |= ui.button("Connect").clicked(); - }); - - if connect { - let mut target = ui_state.target.clone(); - if target.is_empty() { - DEFAULT_TARGET.clone_into(&mut target); - } - - let config = web_socket_config(); - - global_ui.session_id += 1; - let name = format!("{}. {target}", global_ui.session_id); - commands - .spawn((Name::new(name), AeronetRepliconClient)) - .queue(WebSocketClient::connect(config, target)); - } - }); -} - -type WebSocketClientConfig = aeronet_websocket::client::ClientConfig; - -#[cfg(target_family = "wasm")] -fn web_socket_config() -> WebSocketClientConfig { - WebSocketClientConfig::default() -} - -#[cfg(not(target_family = "wasm"))] -fn web_socket_config() -> WebSocketClientConfig { - WebSocketClientConfig::builder().with_no_cert_validation() -} - -// -// game logic -// - fn handle_inputs(mut inputs: EventWriter, input: Res>) { let mut movement = Vec2::ZERO; if input.just_released(KeyCode::ArrowRight) { diff --git a/minigolf_client/src/network.rs b/minigolf_client/src/network.rs new file mode 100644 index 0000000..7b500b4 --- /dev/null +++ b/minigolf_client/src/network.rs @@ -0,0 +1,87 @@ +use { + aeronet_replicon::client::AeronetRepliconClientPlugin, + aeronet_websocket::client::WebSocketClientPlugin, + aeronet_webtransport::{cert, client::WebTransportClientPlugin}, + bevy::prelude::*, + bevy_replicon::prelude::*, +}; + +/// Sets up minigolf client networking. +#[derive(Debug)] +pub(crate) struct ClientNetworkPlugin; + +impl Plugin for ClientNetworkPlugin { + fn build(&self, app: &mut App) { + app.add_plugins((WebTransportClientPlugin, WebSocketClientPlugin)) + .add_plugins((RepliconPlugins, AeronetRepliconClientPlugin)); + } +} + +// +// WebTransport +// + +type WebTransportClientConfig = aeronet_webtransport::client::ClientConfig; + +#[cfg(target_family = "wasm")] +pub(crate) fn web_transport_config(cert_hash: String) -> WebTransportClientConfig { + use aeronet_webtransport::xwt_web::{CertificateHash, HashAlgorithm}; + + let server_certificate_hashes = match cert::hash_from_b64(&cert_hash) { + Ok(hash) => vec![CertificateHash { + algorithm: HashAlgorithm::Sha256, + value: Vec::from(hash), + }], + Err(err) => { + warn!("Failed to read certificate hash from string: {err:?}"); + Vec::new() + } + }; + + WebTransportClientConfig { + server_certificate_hashes, + ..Default::default() + } +} + +#[cfg(not(target_family = "wasm"))] +pub(crate) fn web_transport_config(cert_hash: String) -> WebTransportClientConfig { + use {aeronet_webtransport::wtransport::tls::Sha256Digest, core::time::Duration}; + + let config = WebTransportClientConfig::builder().with_bind_default(); + + let config = if cert_hash.is_empty() { + warn!("Connecting without certificate validation"); + config.with_no_cert_validation() + } else { + match cert::hash_from_b64(&cert_hash) { + Ok(hash) => config.with_server_certificate_hashes([Sha256Digest::new(hash)]), + Err(err) => { + warn!("Failed to read certificate hash from string: {err:?}"); + config.with_server_certificate_hashes([]) + } + } + }; + + config + .keep_alive_interval(Some(Duration::from_secs(1))) + .max_idle_timeout(Some(Duration::from_secs(5))) + .expect("should be a valid idle timeout") + .build() +} + +// +// WebSocket +// + +type WebSocketClientConfig = aeronet_websocket::client::ClientConfig; + +#[cfg(target_family = "wasm")] +pub(crate) fn web_socket_config() -> WebSocketClientConfig { + WebSocketClientConfig::default() +} + +#[cfg(not(target_family = "wasm"))] +pub(crate) fn web_socket_config() -> WebSocketClientConfig { + WebSocketClientConfig::builder().with_no_cert_validation() +} diff --git a/minigolf_client/src/ui.rs b/minigolf_client/src/ui.rs new file mode 100644 index 0000000..7912cb0 --- /dev/null +++ b/minigolf_client/src/ui.rs @@ -0,0 +1,249 @@ +use { + crate::network::{web_socket_config, web_transport_config}, + aeronet::io::{ + Session, SessionEndpoint, + connection::{Disconnect, DisconnectReason, Disconnected}, + }, + aeronet_replicon::client::AeronetRepliconClient, + aeronet_websocket::client::WebSocketClient, + aeronet_webtransport::client::WebTransportClient, + bevy::{ + ecs::query::QuerySingleError, input::common_conditions::input_toggle_active, prelude::*, + }, + bevy_egui::{EguiContexts, EguiPlugin, egui}, + bevy_inspector_egui::quick::WorldInspectorPlugin, + bevy_replicon::prelude::*, +}; + +/// Sets up minigolf client UI. +#[derive(Debug)] +pub(crate) struct ClientUiPlugin; + +impl Plugin for ClientUiPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(EguiPlugin) + .add_plugins( + WorldInspectorPlugin::default().run_if(input_toggle_active(true, KeyCode::Escape)), + ) + .init_resource::() + .init_resource::() + .init_resource::() + .add_systems(Update, (web_transport_ui, web_socket_ui, global_ui).chain()) + .add_observer(on_connecting) + .add_observer(on_connected) + .add_observer(on_disconnected); + } +} + +#[derive(Debug, Default, Resource)] +struct GlobalUi { + session_id: usize, + log: Vec, +} + +#[derive(Debug, Default, Resource)] +struct WebTransportUi { + target: String, + cert_hash: String, +} + +#[derive(Debug, Default, Resource)] +struct WebSocketUi { + target: String, +} + +fn on_connecting( + trigger: Trigger, + names: Query<&Name>, + mut ui_state: ResMut, +) { + let entity = trigger.entity(); + let name = names + .get(entity) + .expect("our session entity should have a name"); + ui_state.log.push(format!("{name} connecting")); +} + +fn on_connected( + trigger: Trigger, + names: Query<&Name>, + mut ui_state: ResMut, +) { + let entity = trigger.entity(); + let name = names + .get(entity) + .expect("our session entity should have a name"); + ui_state.log.push(format!("{name} connected")); +} + +fn on_disconnected( + trigger: Trigger, + names: Query<&Name>, + mut ui_state: ResMut, +) { + let session = trigger.entity(); + let name = names + .get(session) + .expect("our session entity should have a name"); + ui_state.log.push(match &trigger.reason { + DisconnectReason::User(reason) => { + format!("{name} disconnected by user: {reason}") + } + DisconnectReason::Peer(reason) => { + format!("{name} disconnected by peer: {reason}") + } + DisconnectReason::Error(err) => { + format!("{name} disconnected due to error: {err:?}") + } + }); +} + +fn global_ui( + mut commands: Commands, + mut egui: EguiContexts, + global_ui: Res, + sessions: Query<(Entity, &Name, Option<&Session>), With>, + replicon_client: Res, +) { + let stats = replicon_client.stats(); + egui::Window::new("Session Log").show(egui.ctx_mut(), |ui| { + ui.label("Replicon reports:"); + ui.horizontal(|ui| { + ui.label(match replicon_client.status() { + RepliconClientStatus::Disconnected => "Disconnected", + RepliconClientStatus::Connecting => "Connecting", + RepliconClientStatus::Connected { .. } => "Connected", + }); + ui.separator(); + + ui.label(format!("RTT {:.0}ms", stats.rtt * 1000.0)); + ui.separator(); + + ui.label(format!("Pkt Loss {:.1}%", stats.packet_loss * 100.0)); + ui.separator(); + + ui.label(format!("Rx {:.0}bps", stats.received_bps)); + ui.separator(); + + ui.label(format!("Tx {:.0}bps", stats.sent_bps)); + }); + match sessions.get_single() { + Ok((session, name, connected)) => { + if connected.is_some() { + ui.label(format!("{name} connected")); + } else { + ui.label(format!("{name} connecting")); + } + + if ui.button("Disconnect").clicked() { + commands.trigger_targets(Disconnect::new("disconnected by user"), session); + } + } + Err(QuerySingleError::NoEntities(_)) => { + ui.label("No sessions active"); + } + Err(QuerySingleError::MultipleEntities(_)) => { + ui.label("Multiple sessions active"); + } + } + + ui.separator(); + + for msg in &global_ui.log { + ui.label(msg); + } + }); +} + +fn web_transport_ui( + mut commands: Commands, + mut egui: EguiContexts, + mut global_ui: ResMut, + mut ui_state: ResMut, + sessions: Query<(), With>, +) { + const DEFAULT_TARGET: &str = "https://remote-dev:25565"; + + egui::Window::new("WebTransport").show(egui.ctx_mut(), |ui| { + if sessions.iter().next().is_some() { + ui.disable(); + } + + let enter_pressed = ui.input(|i| i.key_pressed(egui::Key::Enter)); + + let mut connect = false; + ui.horizontal(|ui| { + let connect_resp = ui.add( + egui::TextEdit::singleline(&mut ui_state.target) + .hint_text(format!("{DEFAULT_TARGET} | [enter] to connect")), + ); + connect |= connect_resp.lost_focus() && enter_pressed; + connect |= ui.button("Connect").clicked(); + }); + + let cert_hash_resp = ui.add( + egui::TextEdit::singleline(&mut ui_state.cert_hash) + .hint_text("(optional) certificate hash"), + ); + connect |= cert_hash_resp.lost_focus() && enter_pressed; + + if connect { + let mut target = ui_state.target.clone(); + if target.is_empty() { + DEFAULT_TARGET.clone_into(&mut target); + } + + let cert_hash = ui_state.cert_hash.clone(); + let config = web_transport_config(cert_hash); + + global_ui.session_id += 1; + let name = format!("{}. {target}", global_ui.session_id); + commands + .spawn((Name::new(name), AeronetRepliconClient)) + .queue(WebTransportClient::connect(config, target)); + } + }); +} + +fn web_socket_ui( + mut commands: Commands, + mut egui: EguiContexts, + mut global_ui: ResMut, + mut ui_state: ResMut, + sessions: Query<(), With>, +) { + const DEFAULT_TARGET: &str = "ws://remote-dev:25566"; + + egui::Window::new("WebSocket").show(egui.ctx_mut(), |ui| { + if sessions.iter().next().is_some() { + ui.disable(); + } + + let enter_pressed = ui.input(|i| i.key_pressed(egui::Key::Enter)); + + let mut connect = false; + ui.horizontal(|ui| { + let connect_resp = ui.add( + egui::TextEdit::singleline(&mut ui_state.target) + .hint_text(format!("{DEFAULT_TARGET} | [enter] to connect")), + ); + connect |= connect_resp.lost_focus() && enter_pressed; + connect |= ui.button("Connect").clicked(); + }); + + if connect { + let mut target = ui_state.target.clone(); + if target.is_empty() { + DEFAULT_TARGET.clone_into(&mut target); + } + + let config = web_socket_config(); + + global_ui.session_id += 1; + let name = format!("{}. {target}", global_ui.session_id); + commands + .spawn((Name::new(name), AeronetRepliconClient)) + .queue(WebSocketClient::connect(config, target)); + } + }); +} diff --git a/minigolf_server/src/main.rs b/minigolf_server/src/main.rs index cb78f93..791ae0e 100644 --- a/minigolf_server/src/main.rs +++ b/minigolf_server/src/main.rs @@ -3,6 +3,8 @@ fn main() { panic!("this example is not available on WASM"); } +#[cfg(not(target_family = "wasm"))] +mod network; #[cfg(not(target_family = "wasm"))] mod server; diff --git a/minigolf_server/src/network.rs b/minigolf_server/src/network.rs new file mode 100644 index 0000000..29a06ef --- /dev/null +++ b/minigolf_server/src/network.rs @@ -0,0 +1,148 @@ +use { + crate::server::Args, + aeronet::io::{ + Session, + connection::{DisconnectReason, Disconnected, LocalAddr}, + server::Server, + }, + aeronet_replicon::server::{AeronetRepliconServer, AeronetRepliconServerPlugin}, + aeronet_websocket::server::{WebSocketServer, WebSocketServerPlugin}, + aeronet_webtransport::{ + cert, + server::{SessionRequest, SessionResponse, WebTransportServer, WebTransportServerPlugin}, + wtransport, + }, + bevy::prelude::*, + bevy_replicon::prelude::*, + core::time::Duration, +}; + +/// Sets up minigolf server networking. +#[derive(Debug)] +pub(crate) struct ServerNetworkPlugin; + +impl Plugin for ServerNetworkPlugin { + fn build(&self, app: &mut App) { + app.add_plugins((WebTransportServerPlugin, WebSocketServerPlugin)) + .add_plugins(( + RepliconPlugins.set(ServerPlugin { + // 1 frame lasts `1.0 / TICK_RATE` anyway + tick_policy: TickPolicy::Manual, + ..Default::default() + }), + AeronetRepliconServerPlugin, + )) + .add_observer(on_opened) + .add_observer(on_session_request) + .add_observer(on_connected) + .add_observer(on_disconnected) + .add_systems(Startup, (open_web_transport_server, open_web_socket_server)); + } +} + +// +// WebTransport +// + +fn open_web_transport_server(mut commands: Commands, args: Res) { + let identity = wtransport::Identity::self_signed(["localhost", "127.0.0.1", "::1"]) + .expect("all given SANs should be valid DNS names"); + let cert = &identity.certificate_chain().as_slice()[0]; + let spki_fingerprint = cert::spki_fingerprint_b64(cert).expect("should be a valid certificate"); + let cert_hash = cert::hash_to_b64(cert.hash()); + info!("************************"); + info!("SPKI FINGERPRINT"); + info!(" {spki_fingerprint}"); + info!("CERTIFICATE HASH"); + info!(" {cert_hash}"); + info!("************************"); + + let config = web_transport_config(identity, &args); + let server = commands + .spawn((Name::new("WebTransport Server"), AeronetRepliconServer)) + .queue(WebTransportServer::open(config)) + .id(); + info!("Opening WebTransport server {server}"); +} + +type WebTransportServerConfig = aeronet_webtransport::server::ServerConfig; + +fn web_transport_config(identity: wtransport::Identity, args: &Args) -> WebTransportServerConfig { + WebTransportServerConfig::builder() + .with_bind_default(args.wt_port) + .with_identity(identity) + .keep_alive_interval(Some(Duration::from_secs(1))) + .max_idle_timeout(Some(Duration::from_secs(5))) + .expect("should be a valid idle timeout") + .build() +} + +// +// WebSocket +// + +type WebSocketServerConfig = aeronet_websocket::server::ServerConfig; + +fn open_web_socket_server(mut commands: Commands, args: Res) { + let config = web_socket_config(&args); + let server = commands + .spawn((Name::new("WebSocket Server"), AeronetRepliconServer)) + .queue(WebSocketServer::open(config)) + .id(); + info!("Opening WebSocket server {server}"); +} + +fn web_socket_config(args: &Args) -> WebSocketServerConfig { + WebSocketServerConfig::builder() + .with_bind_default(args.ws_port) + .with_no_encryption() +} + +fn on_opened(trigger: Trigger, servers: Query<&LocalAddr>) { + let server = trigger.entity(); + let local_addr = servers + .get(server) + .expect("opened server should have a binding socket `LocalAddr`"); + info!("{server} opened on {}", **local_addr); +} + +fn on_session_request(mut request: Trigger, clients: Query<&Parent>) { + let client = request.entity(); + let Ok(server) = clients.get(client).map(Parent::get) else { + return; + }; + + info!("{client} connecting to {server} with headers:"); + for (header_key, header_value) in &request.headers { + info!(" {header_key}: {header_value}"); + } + + request.respond(SessionResponse::Accepted); +} + +fn on_connected(trigger: Trigger, clients: Query<&Parent>) { + let client = trigger.entity(); + let Ok(server) = clients.get(client).map(Parent::get) else { + return; + }; + info!("{client} connected to {server}"); +} + +fn on_disconnected(trigger: Trigger, clients: Query<&Parent>) { + let client = trigger.entity(); + let Ok(server) = clients.get(client).map(Parent::get) else { + return; + }; + + match &trigger.reason { + DisconnectReason::User(reason) => { + info!("{client} disconnected from {server} by user: {reason}"); + } + DisconnectReason::Peer(reason) => { + info!("{client} disconnected from {server} by peer: {reason}"); + } + DisconnectReason::Error(err) => { + warn!("{client} disconnected from {server} due to error: {err:?}"); + } + } +} diff --git a/minigolf_server/src/server.rs b/minigolf_server/src/server.rs index f1dd371..1e21528 100644 --- a/minigolf_server/src/server.rs +++ b/minigolf_server/src/server.rs @@ -1,16 +1,6 @@ use { - aeronet::io::{ - Session, - connection::{DisconnectReason, Disconnected, LocalAddr}, - server::Server, - }, - aeronet_replicon::server::{AeronetRepliconServer, AeronetRepliconServerPlugin}, - aeronet_websocket::server::{WebSocketServer, WebSocketServerPlugin}, - aeronet_webtransport::{ - cert, - server::{SessionRequest, SessionResponse, WebTransportServer, WebTransportServerPlugin}, - wtransport, - }, + crate::network::ServerNetworkPlugin, + aeronet::io::{Session, connection::Disconnected}, avian3d::prelude::*, bevy::{ app::ScheduleRunnerPlugin, @@ -19,7 +9,7 @@ use { render::{RenderPlugin, settings::WgpuSettings}, winit::WinitPlugin, }, - bevy_replicon::prelude::*, + bevy_replicon::{prelude::*, server::increment_tick}, core::time::Duration, minigolf::{LevelMesh, MinigolfPlugin, Player, PlayerInput, TICK_RATE}, }; @@ -37,13 +27,13 @@ enum GameLayer { /// `move_box` demo server #[derive(Debug, Resource, clap::Parser)] -struct Args { +pub(crate) struct Args { /// Port to listen for WebTransport connections on #[arg(long, default_value_t = WEB_TRANSPORT_PORT)] - wt_port: u16, + pub(crate) wt_port: u16, /// Port to listen for WebSocket connections on #[arg(long, default_value_t = WEB_SOCKET_PORT)] - ws_port: u16, + pub(crate) ws_port: u16, } impl FromWorld for Args { @@ -52,6 +42,11 @@ impl FromWorld for Args { } } +#[derive(Component, Reflect)] +struct PlayerSession { + player: Entity, +} + pub fn main() -> AppExit { App::new() .init_resource::() @@ -68,38 +63,20 @@ pub fn main() -> AppExit { .disable::(), ) .add_plugins(( - // transport - WebTransportServerPlugin, - WebSocketServerPlugin, - // replication - RepliconPlugins.set(ServerPlugin { - // 1 frame lasts `1.0 / TICK_RATE` anyway - tick_policy: TickPolicy::Manual, - ..Default::default() - }), - AeronetRepliconServerPlugin, - // game + ServerNetworkPlugin, MinigolfPlugin, PhysicsPlugins::default(), )) .add_plugins(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64( 1.0 / f64::from(TICK_RATE), ))) - .add_systems( - Startup, - (open_web_transport_server, open_web_socket_server, setup), - ) - .add_observer(on_opened) - .add_observer(on_session_request) + .add_systems(Startup, setup) .add_observer(on_connected) .add_observer(on_disconnected) .insert_resource(Time::::from_hz(128.0)) - .add_systems( - FixedUpdate, - recv_input.chain().run_if(server_or_singleplayer), - ) + .add_systems(FixedUpdate, recv_input.run_if(server_or_singleplayer)) .add_systems(FixedUpdate, reset) - .add_systems(FixedPreUpdate, bevy_replicon::server::increment_tick) + .add_systems(FixedPreUpdate, increment_tick) .run() } @@ -181,96 +158,12 @@ fn recv_input( } } -// -// WebTransport -// - -fn open_web_transport_server(mut commands: Commands, args: Res) { - let identity = wtransport::Identity::self_signed(["localhost", "127.0.0.1", "::1"]) - .expect("all given SANs should be valid DNS names"); - let cert = &identity.certificate_chain().as_slice()[0]; - let spki_fingerprint = cert::spki_fingerprint_b64(cert).expect("should be a valid certificate"); - let cert_hash = cert::hash_to_b64(cert.hash()); - info!("************************"); - info!("SPKI FINGERPRINT"); - info!(" {spki_fingerprint}"); - info!("CERTIFICATE HASH"); - info!(" {cert_hash}"); - info!("************************"); - - let config = web_transport_config(identity, &args); - let server = commands - .spawn((Name::new("WebTransport Server"), AeronetRepliconServer)) - .queue(WebTransportServer::open(config)) - .id(); - info!("Opening WebTransport server {server}"); -} - -type WebTransportServerConfig = aeronet_webtransport::server::ServerConfig; - -fn web_transport_config(identity: wtransport::Identity, args: &Args) -> WebTransportServerConfig { - WebTransportServerConfig::builder() - .with_bind_default(args.wt_port) - .with_identity(identity) - .keep_alive_interval(Some(Duration::from_secs(1))) - .max_idle_timeout(Some(Duration::from_secs(5))) - .expect("should be a valid idle timeout") - .build() -} - -fn on_session_request(mut request: Trigger, clients: Query<&Parent>) { - let client = request.entity(); - let Ok(server) = clients.get(client).map(Parent::get) else { - return; - }; - - info!("{client} connecting to {server} with headers:"); - for (header_key, header_value) in &request.headers { - info!(" {header_key}: {header_value}"); - } - - request.respond(SessionResponse::Accepted); -} - -// -// WebSocket -// - -type WebSocketServerConfig = aeronet_websocket::server::ServerConfig; - -fn open_web_socket_server(mut commands: Commands, args: Res) { - let config = web_socket_config(&args); - let server = commands - .spawn((Name::new("WebSocket Server"), AeronetRepliconServer)) - .queue(WebSocketServer::open(config)) - .id(); - info!("Opening WebSocket server {server}"); -} - -fn web_socket_config(args: &Args) -> WebSocketServerConfig { - WebSocketServerConfig::builder() - .with_bind_default(args.ws_port) - .with_no_encryption() -} - // // server logic // -fn on_opened(trigger: Trigger, servers: Query<&LocalAddr>) { - let server = trigger.entity(); - let local_addr = servers - .get(server) - .expect("opened server should have a binding socket `LocalAddr`"); - info!("{server} opened on {}", **local_addr); -} - -fn on_connected(trigger: Trigger, clients: Query<&Parent>, mut commands: Commands) { +fn on_connected(trigger: Trigger, mut commands: Commands) { let client = trigger.entity(); - let Ok(server) = clients.get(client).map(Parent::get) else { - return; - }; - info!("{client} connected to {server}"); let player = commands .spawn(( @@ -292,34 +185,12 @@ fn on_connected(trigger: Trigger, clients: Query<&Parent>, mut c commands.entity(client).insert(PlayerSession { player }); } -#[derive(Component, Reflect)] -struct PlayerSession { - player: Entity, -} - fn on_disconnected( trigger: Trigger, - clients: Query<&Parent>, sessions: Query<&PlayerSession>, mut commands: Commands, ) { let client = trigger.entity(); - let Ok(server) = clients.get(client).map(Parent::get) else { - return; - }; - - match &trigger.reason { - DisconnectReason::User(reason) => { - info!("{client} disconnected from {server} by user: {reason}"); - } - DisconnectReason::Peer(reason) => { - info!("{client} disconnected from {server} by peer: {reason}"); - } - DisconnectReason::Error(err) => { - warn!("{client} disconnected from {server} due to error: {err:?}"); - } - } - let Ok(session) = sessions.get(client) else { return; };