diff --git a/examples/bullet_prespawn/Cargo.toml b/examples/bullet_prespawn/Cargo.toml index 42e23125..9f8264a1 100644 --- a/examples/bullet_prespawn/Cargo.toml +++ b/examples/bullet_prespawn/Cargo.toml @@ -33,3 +33,4 @@ metrics-exporter-prometheus = { version = "0.13.0", optional = true } bevy-inspector-egui = "0.23" cfg-if = "1.0.0" ron = "0.8.1" +crossbeam-channel = "0.5.11" diff --git a/examples/bullet_prespawn/README.md b/examples/bullet_prespawn/README.md index de24964f..2f07e9ec 100644 --- a/examples/bullet_prespawn/README.md +++ b/examples/bullet_prespawn/README.md @@ -1,20 +1,39 @@ # Features - This example showcases how prespawning player objects on the client side works: -- you just have to add a `PreSpawnedPlayedObject` component to the pre-spawned entity. The system that spawns the entity can be identical in the client and the server + +- you just have to add a `PreSpawnedPlayedObject` component to the pre-spawned entity. The system that spawns the entity + can be identical in the client and the server - the client spawns the entity immediately in the predicted timeline - when the client receives the server entity, it will match it with the existing pre-spawned entity! +https://github.com/cBournhonesque/lightyear/assets/8112632/ee547c32-1f14-4bdc-9e6d-67f900af84d0 +## Running the example -https://github.com/cBournhonesque/lightyear/assets/8112632/ee547c32-1f14-4bdc-9e6d-67f900af84d0 +You can either run the example as a "Listen Server" (the program acts as both client and server) +with: `cargo run -- listen-server` +or as dedicated server with `cargo run -- server` + +Then you can launch multiple clients with the commands: + +- `cargo run -- client -c 1` +- `cargo run -- client -c 2` + +You can modify the file `assets/settings.ron` to modify some networking settings. + +### Testing webtransport in wasm +NOTE: I am using [trunk](https://trunkrs.dev/) to build and serve the wasm example. +To test the example in wasm, you can run the following commands: `trunk serve` -# Usage +You will need a valid SSL certificate to test the example in wasm using webtransport. You will need to run the following +commands: -- Run the server with: `cargo run -- server --headless` -- Run the clients with: -`cargo run -- client -c 1` -`cargo run -- client -c 2` +- `sh examples/generate.sh` (to generate the temporary SSL certificates, they are only valid for 2 weeks) +- `cargo run -- server` to start the server. The server will print out the certificate digest (something + like `1fd28860bd2010067cee636a64bcbb492142295b297fd8c480e604b70ce4d644`) +- You then have to replace the certificate digest in the `assets/settings.ron` file with the one that the server printed + out. +- then start the client wasm test with `trunk serve` \ No newline at end of file diff --git a/examples/bullet_prespawn/src/client.rs b/examples/bullet_prespawn/src/client.rs index a7f173b8..19c81bf1 100644 --- a/examples/bullet_prespawn/src/client.rs +++ b/examples/bullet_prespawn/src/client.rs @@ -1,6 +1,6 @@ -use crate::protocol::*; -use crate::shared::{color_from_id, shared_config, shared_player_movement}; -use crate::{shared, ClientTransports, SharedSettings, KEY, PROTOCOL_ID}; +use std::net::{Ipv4Addr, SocketAddr}; +use std::str::FromStr; + use bevy::app::PluginGroupBuilder; use bevy::ecs::schedule::{LogLevel, ScheduleBuildSettings}; use bevy::prelude::*; @@ -11,12 +11,15 @@ use leafwing_input_manager::buttonlike::ButtonState::Pressed; use leafwing_input_manager::orientation::Orientation; use leafwing_input_manager::plugin::InputManagerSystem; use leafwing_input_manager::prelude::*; + use lightyear::inputs::native::input_buffer::InputBuffer; use lightyear::prelude::client::LeafwingInputPlugin; use lightyear::prelude::client::*; use lightyear::prelude::*; -use std::net::{Ipv4Addr, SocketAddr}; -use std::str::FromStr; + +use crate::protocol::*; +use crate::shared::{color_from_id, shared_config, shared_player_movement}; +use crate::{shared, ClientTransports, SharedSettings}; pub const INPUT_DELAY_TICKS: u16 = 0; pub const CORRECTION_TICKS_FACTOR: f32 = 1.5; @@ -28,25 +31,10 @@ pub struct ClientPluginGroup { impl ClientPluginGroup { pub(crate) fn new( client_id: u64, - client_port: u16, server_addr: SocketAddr, - transport: ClientTransports, + transport_config: TransportConfig, shared_settings: SharedSettings, ) -> ClientPluginGroup { - let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), client_port); - let transport_config = match transport { - #[cfg(not(target_family = "wasm"))] - ClientTransports::Udp => TransportConfig::UdpSocket(client_addr), - ClientTransports::WebTransport { certificate_digest } => { - TransportConfig::WebTransportClient { - client_addr, - server_addr, - #[cfg(target_family = "wasm")] - certificate_digest, - } - } - ClientTransports::WebSocket => TransportConfig::WebSocketClient { server_addr }, - }; let auth = Authentication::Manual { server_addr, client_id, diff --git a/examples/bullet_prespawn/src/main.rs b/examples/bullet_prespawn/src/main.rs index 6d8f0c29..f75129e8 100644 --- a/examples/bullet_prespawn/src/main.rs +++ b/examples/bullet_prespawn/src/main.rs @@ -5,18 +5,11 @@ //! Run with //! - `cargo run -- server` //! - `cargo run -- client -c 1` - -mod client; -mod protocol; -#[cfg(not(target_family = "wasm"))] -mod server; -mod shared; - -use async_compat::Compat; -use std::fs; use std::net::{Ipv4Addr, SocketAddr}; use std::str::FromStr; +use async_compat::Compat; +use bevy::asset::ron; use bevy::log::{Level, LogPlugin}; use bevy::prelude::*; use bevy::tasks::IoTaskPool; @@ -25,16 +18,22 @@ use bevy_inspector_egui::quick::WorldInspectorPlugin; use clap::{Parser, ValueEnum}; use serde::{Deserialize, Serialize}; +use lightyear::connection::netcode::ClientId; +use lightyear::prelude::server::Certificate; +use lightyear::prelude::TransportConfig; +use lightyear::shared::log::add_log_layer; +use lightyear::transport::LOCAL_SOCKET; + use crate::client::ClientPluginGroup; #[cfg(not(target_family = "wasm"))] use crate::server::ServerPluginGroup; -use lightyear::connection::netcode::{ClientId, Key}; -use lightyear::prelude::TransportConfig; -use lightyear::shared::log::add_log_layer; -pub const PROTOCOL_ID: u64 = 0; +mod client; +mod protocol; -pub const KEY: Key = [0; 32]; +#[cfg(not(target_family = "wasm"))] +mod server; +mod shared; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum ClientTransports { @@ -53,7 +52,7 @@ pub enum ServerTransports { WebSocket { local_port: u16 }, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct ServerSettings { /// If true, disable any rendering-related plugins headless: bool, @@ -65,7 +64,7 @@ pub struct ServerSettings { transport: Vec, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct ClientSettings { /// If true, enable bevy_inspector_egui inspector: bool, @@ -95,7 +94,7 @@ pub struct SharedSettings { private_key: [u8; 32], } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Settings { pub server: ServerSettings, pub client: ClientSettings, @@ -105,7 +104,15 @@ pub struct Settings { #[derive(Parser, PartialEq, Debug)] enum Cli { #[cfg(not(target_family = "wasm"))] + /// The program will act both as a server and as a client + ListenServer { + #[arg(short, long, default_value = None)] + client_id: Option, + }, + #[cfg(not(target_family = "wasm"))] + /// Dedicated server Server, + /// The program will act as a client Client { #[arg(short, long, default_value = None)] client_id: Option, @@ -125,66 +132,157 @@ fn main() { } let settings_str = include_str!("../assets/settings.ron"); let settings = ron::de::from_str::(settings_str).unwrap(); - let mut app = App::new(); - setup(&mut app, settings, cli); - app.run(); + run(settings, cli); } -fn setup(app: &mut App, settings: Settings, cli: Cli) { +fn run(settings: Settings, cli: Cli) { match cli { + #[cfg(not(target_family = "wasm"))] + Cli::ListenServer { client_id } => { + // create client app + let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); + let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); + let transport_config = TransportConfig::LocalChannel { + recv: from_server_recv, + send: to_server_send, + }; + // when communicating via channels, we need to use the address `LOCAL_SOCKET` for the server + let mut client_app = + client_app(settings.clone(), LOCAL_SOCKET, client_id, transport_config); + + // create server app + let extra_transport_configs = vec![TransportConfig::Channels { + // even if we communicate via channels, we need to provide a socket address for the client + channels: vec![(LOCAL_SOCKET, to_server_recv, from_server_send)], + }]; + let mut server_app = server_app(settings, extra_transport_configs); + + // run both the client and server apps + std::thread::spawn(move || server_app.run()); + client_app.run(); + } #[cfg(not(target_family = "wasm"))] Cli::Server => { - let shared = settings.shared; - let settings = settings.server; - if !settings.headless { - app.add_plugins(DefaultPlugins.build().disable::()); - } else { - app.add_plugins(MinimalPlugins); - } - app.add_plugins(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), - update_subscriber: Some(add_log_layer), - }); - - if settings.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - // this is async because we need to load the certificate from io - // we need async_compat because wtransport expects a tokio reactor - let server_plugin_group = IoTaskPool::get() - .scope(|s| { - s.spawn(Compat::new(async { - ServerPluginGroup::new(settings.transport, shared).await - })); - }) - .pop() - .unwrap(); - app.add_plugins(server_plugin_group.build()); + let mut app = server_app(settings, vec![]); + app.run(); } Cli::Client { client_id } => { - let shared = settings.shared; - let settings = settings.client; - // NOTE: create the default plugins first so that the async task pools are initialized - // use the default bevy logger for now - // (the lightyear logger doesn't handle wasm) - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.inspector { - app.add_plugins(WorldInspectorPlugin::new()); + let server_addr = SocketAddr::new( + settings.client.server_addr.into(), + settings.client.server_port, + ); + let transport_config = get_client_transport_config(settings.client.clone()); + let mut app = client_app(settings, server_addr, client_id, transport_config); + app.run(); + } + } +} + +/// Build the client app +fn client_app( + settings: Settings, + server_addr: SocketAddr, + client_id: Option, + transport_config: TransportConfig, +) -> App { + let mut app = App::new(); + // NOTE: create the default plugins first so that the async task pools are initialized + // use the default bevy logger for now + // (the lightyear logger doesn't handle wasm) + app.add_plugins(DefaultPlugins.build().set(LogPlugin { + level: Level::INFO, + filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), + update_subscriber: Some(add_log_layer), + })); + if settings.client.inspector { + app.add_plugins(WorldInspectorPlugin::new()); + } + let client_plugin_group = ClientPluginGroup::new( + // use the cli-provided client id if it exists, otherwise use the settings client id + client_id.unwrap_or(settings.client.client_id), + server_addr, + transport_config, + settings.shared, + ); + app.add_plugins(client_plugin_group.build()); + app +} + +/// Build the server app +fn server_app(settings: Settings, extra_transport_configs: Vec) -> App { + let mut app = App::new(); + if !settings.server.headless { + app.add_plugins(DefaultPlugins.build().disable::()); + } else { + app.add_plugins(MinimalPlugins); + } + app.add_plugins(LogPlugin { + level: Level::INFO, + filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), + update_subscriber: Some(add_log_layer), + }); + + if settings.server.inspector { + app.add_plugins(WorldInspectorPlugin::new()); + } + let mut transport_configs = get_server_transport_configs(settings.server.transport); + transport_configs.extend(extra_transport_configs); + let server_plugin_group = ServerPluginGroup::new(transport_configs, settings.shared); + app.add_plugins(server_plugin_group.build()); + app +} + +/// Parse the server transport settings into a list of `TransportConfig` that are used to configure the lightyear server +fn get_server_transport_configs(settings: Vec) -> Vec { + settings + .iter() + .map(|t| match t { + ServerTransports::Udp { local_port } => TransportConfig::UdpSocket(SocketAddr::new( + Ipv4Addr::UNSPECIFIED.into(), + *local_port, + )), + ServerTransports::WebTransport { local_port } => { + // this is async because we need to load the certificate from io + // we need async_compat because wtransport expects a tokio reactor + let certificate = IoTaskPool::get() + .scope(|s| { + s.spawn(Compat::new(async { + Certificate::load("../certificates/cert.pem", "../certificates/key.pem") + .await + .unwrap() + })); + }) + .pop() + .unwrap(); + let digest = &certificate.hashes()[0].to_string().replace(":", ""); + println!("Generated self-signed certificate with digest: {}", digest); + TransportConfig::WebTransportServer { + server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), + certificate, + } } - let server_addr = SocketAddr::new(settings.server_addr.into(), settings.server_port); - let client_plugin_group = ClientPluginGroup::new( - client_id.unwrap_or(settings.client_id), - settings.client_port, + ServerTransports::WebSocket { local_port } => TransportConfig::WebSocketServer { + server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), + }, + }) + .collect() +} + +/// Parse the client transport settings into a `TransportConfig` that is used to configure the lightyear client +fn get_client_transport_config(settings: ClientSettings) -> TransportConfig { + let server_addr = SocketAddr::new(settings.server_addr.into(), settings.server_port); + let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), settings.client_port); + match settings.transport { + #[cfg(not(target_family = "wasm"))] + ClientTransports::Udp => TransportConfig::UdpSocket(client_addr), + ClientTransports::WebTransport { certificate_digest } => { + TransportConfig::WebTransportClient { + client_addr, server_addr, - settings.transport, - shared, - ); - app.add_plugins(client_plugin_group.build()); + #[cfg(target_family = "wasm")] + certificate_digest, + } } + ClientTransports::WebSocket => TransportConfig::WebSocketClient { server_addr }, } } diff --git a/examples/bullet_prespawn/src/protocol.rs b/examples/bullet_prespawn/src/protocol.rs index 39205584..615ff73d 100644 --- a/examples/bullet_prespawn/src/protocol.rs +++ b/examples/bullet_prespawn/src/protocol.rs @@ -1,11 +1,12 @@ use bevy::prelude::*; use derive_more::{Add, Mul}; use leafwing_input_manager::prelude::*; +use serde::{Deserialize, Serialize}; + use lightyear::client::components::LerpFn; use lightyear::prelude::*; use lightyear::shared::replication::components::ReplicationGroupIdBuilder; use lightyear::utils::bevy::*; -use serde::{Deserialize, Serialize}; pub const BALL_SIZE: f32 = 10.0; pub const PLAYER_SIZE: f32 = 40.0; diff --git a/examples/bullet_prespawn/src/server.rs b/examples/bullet_prespawn/src/server.rs index ec96d4b3..33942b73 100644 --- a/examples/bullet_prespawn/src/server.rs +++ b/examples/bullet_prespawn/src/server.rs @@ -1,16 +1,19 @@ -use crate::protocol::*; -use crate::shared::{color_from_id, shared_config, shared_player_movement}; -use crate::{shared, ServerTransports, SharedSettings, KEY, PROTOCOL_ID}; +use std::collections::HashMap; +use std::net::{Ipv4Addr, SocketAddr}; + use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use bevy::utils::Duration; use leafwing_input_manager::prelude::*; + use lightyear::client::prediction::Predicted; use lightyear::prelude::server::*; use lightyear::prelude::*; use lightyear::server::config::PacketConfig; -use std::collections::HashMap; -use std::net::{Ipv4Addr, SocketAddr}; + +use crate::protocol::*; +use crate::shared::{color_from_id, shared_config, shared_player_movement}; +use crate::{shared, ServerTransports, SharedSettings}; // Plugin group to add all server-related plugins pub struct ServerPluginGroup { @@ -18,8 +21,8 @@ pub struct ServerPluginGroup { } impl ServerPluginGroup { - pub(crate) async fn new( - transports: Vec, + pub(crate) fn new( + transport_configs: Vec, shared_settings: SharedSettings, ) -> ServerPluginGroup { // Step 1: create the io (transport + link conditioner) @@ -29,31 +32,7 @@ impl ServerPluginGroup { incoming_loss: 0.02, }; let mut net_configs = vec![]; - for transport in &transports { - let transport_config = match transport { - ServerTransports::Udp { local_port } => { - let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port); - TransportConfig::UdpSocket(server_addr) - } - // if using webtransport, we load the certificate keys - ServerTransports::WebTransport { local_port } => { - let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port); - let certificate = - Certificate::load("../certificates/cert.pem", "../certificates/key.pem") - .await - .unwrap(); - let digest = &certificate.hashes()[0].to_string().replace(":", ""); - println!("Generated self-signed certificate with digest: {}", digest); - TransportConfig::WebTransportServer { - server_addr, - certificate, - } - } - ServerTransports::WebSocket { local_port } => { - let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port); - TransportConfig::WebSocketServer { server_addr } - } - }; + for transport_config in transport_configs { net_configs.push(NetConfig::Netcode { config: NetcodeConfig::default() .with_protocol_id(shared_settings.protocol_id) diff --git a/examples/bullet_prespawn/src/shared.rs b/examples/bullet_prespawn/src/shared.rs index 673711c4..35e8f8f7 100644 --- a/examples/bullet_prespawn/src/shared.rs +++ b/examples/bullet_prespawn/src/shared.rs @@ -1,4 +1,3 @@ -use crate::protocol::*; use bevy::diagnostic::LogDiagnosticsPlugin; use bevy::prelude::*; use bevy::render::RenderPlugin; @@ -6,13 +5,16 @@ use bevy::utils::Duration; use bevy_screen_diagnostics::{Aggregate, ScreenDiagnostics, ScreenDiagnosticsPlugin}; use leafwing_input_manager::orientation::Orientation; use leafwing_input_manager::prelude::ActionState; +use tracing::Level; + use lightyear::client::prediction::plugin::is_in_rollback; use lightyear::client::prediction::{Rollback, RollbackState}; use lightyear::prelude::client::*; use lightyear::prelude::TickManager; use lightyear::prelude::*; use lightyear::transport::io::IoDiagnosticsPlugin; -use tracing::Level; + +use crate::protocol::*; const FRAME_HZ: f64 = 60.0; const FIXED_TIMESTEP_HZ: f64 = 64.0; diff --git a/examples/client_replication/Cargo.toml b/examples/client_replication/Cargo.toml index 90a2a364..6a992891 100644 --- a/examples/client_replication/Cargo.toml +++ b/examples/client_replication/Cargo.toml @@ -36,3 +36,4 @@ mock_instant = "0.3" metrics-exporter-prometheus = { version = "0.13.0", optional = true } bevy-inspector-egui = "0.23" cfg-if = "1.0.0" +crossbeam-channel = "0.5.11" diff --git a/examples/client_replication/README.md b/examples/client_replication/README.md index bb05fb44..7b1a0c6f 100644 --- a/examples/client_replication/README.md +++ b/examples/client_replication/README.md @@ -1,51 +1,50 @@ # Introduction -A simple example that shows how to use lightyear for client-replication (the entity is spawned on the client and replicated to the server): - - with client-authority: the cursor is replicated to the server and to other clients. Any client updates are replicated to the server. - If we want to replicate it to other clients, we just needs to add the `Replicate` component on the server's entity to replicate the cursor to other clients. - - - spawning pre-predicted entities on the client: when pressing the `Space` key, a square is spawned on the client. That square is a 'pre-predicted' entity: - it will get replicated to the server. The server can replicate it back to all clients. - When the original client gets the square back, it will spawn a 'Confirmed' square on the client, and will recognize - that the original square spawned was a prediction. From there on it's normal replication. +A simple example that shows how to use lightyear for client-replication (the entity is spawned on the client and +replicated to the server): - - pressing `M` will send a message from a client to other clients +- with client-authority: the cursor is replicated to the server and to other clients. Any client updates are replicated + to the server. + If we want to replicate it to other clients, we just needs to add the `Replicate` component on the server's entity to + replicate the cursor to other clients. - - pressing `K` will delete the Predicted entity. You can use this to confirm various rollback edge-cases. +- spawning pre-predicted entities on the client: when pressing the `Space` key, a square is spawned on the client. That + square is a 'pre-predicted' entity: + it will get replicated to the server. The server can replicate it back to all clients. + When the original client gets the square back, it will spawn a 'Confirmed' square on the client, and will recognize + that the original square spawned was a prediction. From there on it's normal replication. +- pressing `M` will send a message from a client to other clients -https://github.com/cBournhonesque/lightyear/assets/8112632/718bfa44-80b5-4d83-a360-aae076f81fc3 +- pressing `K` will delete the Predicted entity. You can use this to confirm various rollback edge-cases. +https://github.com/cBournhonesque/lightyear/assets/8112632/718bfa44-80b5-4d83-a360-aae076f81fc3 ## Running the example -NOTE: I am using [trunk](https://trunkrs.dev/) to build and serve the wasm example. - - -To start the server, run `cargo run -- server` +You can either run the example as a "Listen Server" (the program acts as both client and server) +with: `cargo run -- listen-server` +or as dedicated server with `cargo run -- server` Then you can launch multiple clients with the commands: - `cargo run -- client -c 1` +- `cargo run -- client -c 2` -- `cargo run -- client -c 2 --client-port 2000` +You can modify the file `assets/settings.ron` to modify some networking settings. +### Testing webtransport in wasm -### Testing in wasm +NOTE: I am using [trunk](https://trunkrs.dev/) to build and serve the wasm example. +To test the example in wasm, you can run the following commands: `trunk serve` -NOTE: I am using [trunk](https://trunkrs.dev/) to build and serve the wasm example. +You will need a valid SSL certificate to test the example in wasm using webtransport. You will need to run the following +commands: -To test the example in wasm, you can run the following commands: - `sh examples/generate.sh` (to generate the temporary SSL certificates, they are only valid for 2 weeks) -- `cargo run -- server --transport web-transport` to start the server -- You will then need to copy the certificate digest string that is outputted by the server in the logs and paste it in the `examples/interest_management/client.rs` file. - Replace the certificate value like so: -``` -let certificate_digest = -String::from("09945594ec0978bb76891fb5de82106d7928191152777c9fc81bec0406055159"); -``` -- then start the client wasm test with `trunk serve` - -NOTE: -- the wasm example seems to work better in release mode! +- `cargo run -- server` to start the server. The server will print out the certificate digest (something + like `1fd28860bd2010067cee636a64bcbb492142295b297fd8c480e604b70ce4d644`) +- You then have to replace the certificate digest in the `assets/settings.ron` file with the one that the server printed + out. +- then start the client wasm test with `trunk serve` \ No newline at end of file diff --git a/examples/client_replication/assets/settings.ron b/examples/client_replication/assets/settings.ron index 962b2f5a..82756d34 100644 --- a/examples/client_replication/assets/settings.ron +++ b/examples/client_replication/assets/settings.ron @@ -16,7 +16,7 @@ Settings( // transport: WebSocket, ), server: ServerSettings( - headless: false, + headless: true, inspector: false, transport: [ WebTransport( diff --git a/examples/client_replication/src/client.rs b/examples/client_replication/src/client.rs index 4f45a263..a73cc76f 100644 --- a/examples/client_replication/src/client.rs +++ b/examples/client_replication/src/client.rs @@ -1,15 +1,18 @@ -use crate::protocol::Direction; -use crate::protocol::*; -use crate::shared::{color_from_id, shared_config, shared_movement_behaviour}; -use crate::{shared, ClientTransports, SharedSettings, KEY, PROTOCOL_ID}; +use std::net::{Ipv4Addr, SocketAddr}; +use std::str::FromStr; + use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use bevy::utils::Duration; + use lightyear::_reexport::ShouldBeInterpolated; use lightyear::prelude::client::*; use lightyear::prelude::*; -use std::net::{Ipv4Addr, SocketAddr}; -use std::str::FromStr; + +use crate::protocol::Direction; +use crate::protocol::*; +use crate::shared::{color_from_id, shared_config, shared_movement_behaviour}; +use crate::{shared, ClientTransports, SharedSettings}; pub struct ClientPluginGroup { lightyear: ClientPlugin, @@ -18,25 +21,10 @@ pub struct ClientPluginGroup { impl ClientPluginGroup { pub(crate) fn new( client_id: u64, - client_port: u16, server_addr: SocketAddr, - transport: ClientTransports, + transport_config: TransportConfig, shared_settings: SharedSettings, ) -> ClientPluginGroup { - let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), client_port); - let transport_config = match transport { - #[cfg(not(target_family = "wasm"))] - ClientTransports::Udp => TransportConfig::UdpSocket(client_addr), - ClientTransports::WebTransport { certificate_digest } => { - TransportConfig::WebTransportClient { - client_addr, - server_addr, - #[cfg(target_family = "wasm")] - certificate_digest, - } - } - ClientTransports::WebSocket => TransportConfig::WebSocketClient { server_addr }, - }; let auth = Authentication::Manual { server_addr, client_id, diff --git a/examples/client_replication/src/main.rs b/examples/client_replication/src/main.rs index 566cefb3..f75129e8 100644 --- a/examples/client_replication/src/main.rs +++ b/examples/client_replication/src/main.rs @@ -5,17 +5,11 @@ //! Run with //! - `cargo run -- server` //! - `cargo run -- client -c 1` -mod client; -mod protocol; -#[cfg(not(target_family = "wasm"))] -mod server; -mod shared; - -use async_compat::Compat; -use bevy::asset::ron; use std::net::{Ipv4Addr, SocketAddr}; use std::str::FromStr; +use async_compat::Compat; +use bevy::asset::ron; use bevy::log::{Level, LogPlugin}; use bevy::prelude::*; use bevy::tasks::IoTaskPool; @@ -24,17 +18,22 @@ use bevy_inspector_egui::quick::WorldInspectorPlugin; use clap::{Parser, ValueEnum}; use serde::{Deserialize, Serialize}; +use lightyear::connection::netcode::ClientId; +use lightyear::prelude::server::Certificate; +use lightyear::prelude::TransportConfig; +use lightyear::shared::log::add_log_layer; +use lightyear::transport::LOCAL_SOCKET; + use crate::client::ClientPluginGroup; #[cfg(not(target_family = "wasm"))] use crate::server::ServerPluginGroup; -use lightyear::connection::netcode::{ClientId, Key}; -use lightyear::prelude::TransportConfig; -use lightyear::shared::log::add_log_layer; -pub const SERVER_PORT: u16 = 5000; -pub const PROTOCOL_ID: u64 = 0; +mod client; +mod protocol; -pub const KEY: Key = [0; 32]; +#[cfg(not(target_family = "wasm"))] +mod server; +mod shared; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum ClientTransports { @@ -53,7 +52,7 @@ pub enum ServerTransports { WebSocket { local_port: u16 }, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct ServerSettings { /// If true, disable any rendering-related plugins headless: bool, @@ -65,7 +64,7 @@ pub struct ServerSettings { transport: Vec, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct ClientSettings { /// If true, enable bevy_inspector_egui inspector: bool, @@ -95,7 +94,7 @@ pub struct SharedSettings { private_key: [u8; 32], } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Settings { pub server: ServerSettings, pub client: ClientSettings, @@ -105,7 +104,15 @@ pub struct Settings { #[derive(Parser, PartialEq, Debug)] enum Cli { #[cfg(not(target_family = "wasm"))] + /// The program will act both as a server and as a client + ListenServer { + #[arg(short, long, default_value = None)] + client_id: Option, + }, + #[cfg(not(target_family = "wasm"))] + /// Dedicated server Server, + /// The program will act as a client Client { #[arg(short, long, default_value = None)] client_id: Option, @@ -125,66 +132,157 @@ fn main() { } let settings_str = include_str!("../assets/settings.ron"); let settings = ron::de::from_str::(settings_str).unwrap(); - let mut app = App::new(); - setup(&mut app, settings, cli); - app.run(); + run(settings, cli); } -fn setup(app: &mut App, settings: Settings, cli: Cli) { +fn run(settings: Settings, cli: Cli) { match cli { + #[cfg(not(target_family = "wasm"))] + Cli::ListenServer { client_id } => { + // create client app + let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); + let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); + let transport_config = TransportConfig::LocalChannel { + recv: from_server_recv, + send: to_server_send, + }; + // when communicating via channels, we need to use the address `LOCAL_SOCKET` for the server + let mut client_app = + client_app(settings.clone(), LOCAL_SOCKET, client_id, transport_config); + + // create server app + let extra_transport_configs = vec![TransportConfig::Channels { + // even if we communicate via channels, we need to provide a socket address for the client + channels: vec![(LOCAL_SOCKET, to_server_recv, from_server_send)], + }]; + let mut server_app = server_app(settings, extra_transport_configs); + + // run both the client and server apps + std::thread::spawn(move || server_app.run()); + client_app.run(); + } #[cfg(not(target_family = "wasm"))] Cli::Server => { - let shared = settings.shared; - let settings = settings.server; - if !settings.headless { - app.add_plugins(DefaultPlugins.build().disable::()); - } else { - app.add_plugins(MinimalPlugins); - } - app.add_plugins(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), - update_subscriber: Some(add_log_layer), - }); - - if settings.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - // this is async because we need to load the certificate from io - // we need async_compat because wtransport expects a tokio reactor - let server_plugin_group = IoTaskPool::get() - .scope(|s| { - s.spawn(Compat::new(async { - ServerPluginGroup::new(settings.transport, shared).await - })); - }) - .pop() - .unwrap(); - app.add_plugins(server_plugin_group.build()); + let mut app = server_app(settings, vec![]); + app.run(); } Cli::Client { client_id } => { - let shared = settings.shared; - let settings = settings.client; - // NOTE: create the default plugins first so that the async task pools are initialized - // use the default bevy logger for now - // (the lightyear logger doesn't handle wasm) - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.inspector { - app.add_plugins(WorldInspectorPlugin::new()); + let server_addr = SocketAddr::new( + settings.client.server_addr.into(), + settings.client.server_port, + ); + let transport_config = get_client_transport_config(settings.client.clone()); + let mut app = client_app(settings, server_addr, client_id, transport_config); + app.run(); + } + } +} + +/// Build the client app +fn client_app( + settings: Settings, + server_addr: SocketAddr, + client_id: Option, + transport_config: TransportConfig, +) -> App { + let mut app = App::new(); + // NOTE: create the default plugins first so that the async task pools are initialized + // use the default bevy logger for now + // (the lightyear logger doesn't handle wasm) + app.add_plugins(DefaultPlugins.build().set(LogPlugin { + level: Level::INFO, + filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), + update_subscriber: Some(add_log_layer), + })); + if settings.client.inspector { + app.add_plugins(WorldInspectorPlugin::new()); + } + let client_plugin_group = ClientPluginGroup::new( + // use the cli-provided client id if it exists, otherwise use the settings client id + client_id.unwrap_or(settings.client.client_id), + server_addr, + transport_config, + settings.shared, + ); + app.add_plugins(client_plugin_group.build()); + app +} + +/// Build the server app +fn server_app(settings: Settings, extra_transport_configs: Vec) -> App { + let mut app = App::new(); + if !settings.server.headless { + app.add_plugins(DefaultPlugins.build().disable::()); + } else { + app.add_plugins(MinimalPlugins); + } + app.add_plugins(LogPlugin { + level: Level::INFO, + filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), + update_subscriber: Some(add_log_layer), + }); + + if settings.server.inspector { + app.add_plugins(WorldInspectorPlugin::new()); + } + let mut transport_configs = get_server_transport_configs(settings.server.transport); + transport_configs.extend(extra_transport_configs); + let server_plugin_group = ServerPluginGroup::new(transport_configs, settings.shared); + app.add_plugins(server_plugin_group.build()); + app +} + +/// Parse the server transport settings into a list of `TransportConfig` that are used to configure the lightyear server +fn get_server_transport_configs(settings: Vec) -> Vec { + settings + .iter() + .map(|t| match t { + ServerTransports::Udp { local_port } => TransportConfig::UdpSocket(SocketAddr::new( + Ipv4Addr::UNSPECIFIED.into(), + *local_port, + )), + ServerTransports::WebTransport { local_port } => { + // this is async because we need to load the certificate from io + // we need async_compat because wtransport expects a tokio reactor + let certificate = IoTaskPool::get() + .scope(|s| { + s.spawn(Compat::new(async { + Certificate::load("../certificates/cert.pem", "../certificates/key.pem") + .await + .unwrap() + })); + }) + .pop() + .unwrap(); + let digest = &certificate.hashes()[0].to_string().replace(":", ""); + println!("Generated self-signed certificate with digest: {}", digest); + TransportConfig::WebTransportServer { + server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), + certificate, + } } - let server_addr = SocketAddr::new(settings.server_addr.into(), settings.server_port); - let client_plugin_group = ClientPluginGroup::new( - client_id.unwrap_or(settings.client_id), - settings.client_port, + ServerTransports::WebSocket { local_port } => TransportConfig::WebSocketServer { + server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), + }, + }) + .collect() +} + +/// Parse the client transport settings into a `TransportConfig` that is used to configure the lightyear client +fn get_client_transport_config(settings: ClientSettings) -> TransportConfig { + let server_addr = SocketAddr::new(settings.server_addr.into(), settings.server_port); + let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), settings.client_port); + match settings.transport { + #[cfg(not(target_family = "wasm"))] + ClientTransports::Udp => TransportConfig::UdpSocket(client_addr), + ClientTransports::WebTransport { certificate_digest } => { + TransportConfig::WebTransportClient { + client_addr, server_addr, - settings.transport, - shared, - ); - app.add_plugins(client_plugin_group.build()); + #[cfg(target_family = "wasm")] + certificate_digest, + } } + ClientTransports::WebSocket => TransportConfig::WebSocketClient { server_addr }, } } diff --git a/examples/client_replication/src/protocol.rs b/examples/client_replication/src/protocol.rs index 4bae1dca..714c7ee9 100644 --- a/examples/client_replication/src/protocol.rs +++ b/examples/client_replication/src/protocol.rs @@ -1,8 +1,10 @@ -use bevy::prelude::{default, Bundle, Color, Component, Deref, DerefMut, Entity, Vec2}; +use std::ops::Mul; + +use bevy::prelude::{default, Bundle, Color, Component, Deref, DerefMut, Vec2}; use derive_more::{Add, Mul}; -use lightyear::prelude::*; use serde::{Deserialize, Serialize}; -use std::ops::Mul; + +use lightyear::prelude::*; // Player #[derive(Bundle)] diff --git a/examples/client_replication/src/server.rs b/examples/client_replication/src/server.rs index 7410be46..492c5ed7 100644 --- a/examples/client_replication/src/server.rs +++ b/examples/client_replication/src/server.rs @@ -1,13 +1,16 @@ -use crate::protocol::*; -use crate::shared::{color_from_id, shared_config, shared_movement_behaviour}; -use crate::{shared, ServerTransports, SharedSettings, KEY, PROTOCOL_ID}; +use std::collections::HashMap; +use std::net::{Ipv4Addr, SocketAddr}; + use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use bevy::utils::Duration; + use lightyear::prelude::server::*; use lightyear::prelude::*; -use std::collections::HashMap; -use std::net::{Ipv4Addr, SocketAddr}; + +use crate::protocol::*; +use crate::shared::{color_from_id, shared_config, shared_movement_behaviour}; +use crate::{shared, ServerTransports, SharedSettings}; // Plugin group to add all server-related plugins pub struct ServerPluginGroup { @@ -15,8 +18,8 @@ pub struct ServerPluginGroup { } impl ServerPluginGroup { - pub(crate) async fn new( - transports: Vec, + pub(crate) fn new( + transport_configs: Vec, shared_settings: SharedSettings, ) -> ServerPluginGroup { // Step 1: create the io (transport + link conditioner) @@ -26,31 +29,7 @@ impl ServerPluginGroup { incoming_loss: 0.0, }; let mut net_configs = vec![]; - for transport in &transports { - let transport_config = match transport { - ServerTransports::Udp { local_port } => { - let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port); - TransportConfig::UdpSocket(server_addr) - } - // if using webtransport, we load the certificate keys - ServerTransports::WebTransport { local_port } => { - let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port); - let certificate = - Certificate::load("../certificates/cert.pem", "../certificates/key.pem") - .await - .unwrap(); - let digest = &certificate.hashes()[0].to_string().replace(":", ""); - println!("Generated self-signed certificate with digest: {}", digest); - TransportConfig::WebTransportServer { - server_addr, - certificate, - } - } - ServerTransports::WebSocket { local_port } => { - let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port); - TransportConfig::WebSocketServer { server_addr } - } - }; + for transport_config in transport_configs { net_configs.push(NetConfig::Netcode { config: NetcodeConfig::default() .with_protocol_id(shared_settings.protocol_id) diff --git a/examples/client_replication/src/shared.rs b/examples/client_replication/src/shared.rs index a12b0edb..0fef6ec3 100644 --- a/examples/client_replication/src/shared.rs +++ b/examples/client_replication/src/shared.rs @@ -1,10 +1,11 @@ -use crate::protocol::*; use bevy::prelude::*; use bevy::render::RenderPlugin; use bevy::utils::Duration; + use lightyear::prelude::client::Confirmed; use lightyear::prelude::*; -use tracing::Level; + +use crate::protocol::*; pub fn shared_config() -> SharedConfig { SharedConfig { diff --git a/examples/interest_management/Cargo.toml b/examples/interest_management/Cargo.toml index 02bae6e7..700ad741 100644 --- a/examples/interest_management/Cargo.toml +++ b/examples/interest_management/Cargo.toml @@ -37,3 +37,4 @@ mock_instant = "0.3" metrics-exporter-prometheus = { version = "0.13.0", optional = true } bevy-inspector-egui = "0.23" cfg-if = "1.0.0" +crossbeam-channel = "0.5.11" diff --git a/examples/interest_management/README.md b/examples/interest_management/README.md index 1af5ec9b..84c13997 100644 --- a/examples/interest_management/README.md +++ b/examples/interest_management/README.md @@ -11,18 +11,16 @@ https://github.com/cBournhonesque/lightyear/assets/8112632/41a6d102-77a1-4a44-89 ## Running the example -To start the server, run `cargo run -- server -t udp` +You can either run the example as a "Listen Server" (the program acts as both client and server) +with: `cargo run -- listen-server` +or as dedicated server with `cargo run -- server` Then you can launch multiple clients with the commands: -- `cargo run -- client -c 1 -t udp` -- `cargo run -- client -c 2 --client-port 2000 -t udp` - -### Testing webtransport - -- `cargo run -- server` -- `cargo run -- client -c 1` +- `cargo run -- client -c 1` +- `cargo run -- client -c 2` +You can modify the file `assets/settings.ron` to modify some networking settings. ### Testing webtransport in wasm @@ -30,16 +28,14 @@ https://github.com/cBournhonesque/lightyear/assets/8112632/4ee0685b-0ac6-42c8-84 NOTE: I am using [trunk](https://trunkrs.dev/) to build and serve the wasm example. -To test the example in wasm, you can run the following commands: +To test the example in wasm, you can run the following commands: `trunk serve` + +You will need a valid SSL certificate to test the example in wasm using webtransport. You will need to run the following +commands: + - `sh examples/generate.sh` (to generate the temporary SSL certificates, they are only valid for 2 weeks) -- `cargo run -- server` to start the server -- You will then need to copy the certificate digest string that is outputted by the server in the logs and paste it in the `examples/interest_management/client.rs` file. - Replace the certificate value like so: -``` -let certificate_digest = -String::from("09945594ec0978bb76891fb5de82106d7928191152777c9fc81bec0406055159"); -``` -- then start the client wasm test with `trunk serve` - -NOTE: -- the wasm example seems to work better in release mode! +- `cargo run -- server` to start the server. The server will print out the certificate digest (something + like `1fd28860bd2010067cee636a64bcbb492142295b297fd8c480e604b70ce4d644`) +- You then have to replace the certificate digest in the `assets/settings.ron` file with the one that the server printed + out. +- then start the client wasm test with `trunk serve` \ No newline at end of file diff --git a/examples/interest_management/src/client.rs b/examples/interest_management/src/client.rs index 32b9694f..a89510dc 100644 --- a/examples/interest_management/src/client.rs +++ b/examples/interest_management/src/client.rs @@ -1,17 +1,20 @@ -use crate::protocol::*; -use crate::shared::{shared_config, shared_movement_behaviour}; -use crate::{shared, Cli, ClientTransports, SharedSettings, KEY, PROTOCOL_ID}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::str::FromStr; + use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use bevy::utils::Duration; use leafwing_input_manager::plugin::InputManagerSystem; use leafwing_input_manager::prelude::*; use leafwing_input_manager::systems::{run_if_enabled, tick_action_state}; + use lightyear::_reexport::ShouldBeInterpolated; use lightyear::prelude::client::*; use lightyear::prelude::*; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::str::FromStr; + +use crate::protocol::*; +use crate::shared::{shared_config, shared_movement_behaviour}; +use crate::{shared, Cli, ClientTransports, SharedSettings}; pub struct ClientPluginGroup { client_id: ClientId, @@ -21,25 +24,10 @@ pub struct ClientPluginGroup { impl ClientPluginGroup { pub(crate) fn new( client_id: u64, - client_port: u16, server_addr: SocketAddr, - transport: ClientTransports, + transport_config: TransportConfig, shared_settings: SharedSettings, ) -> ClientPluginGroup { - let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), client_port); - let transport_config = match transport { - #[cfg(not(target_family = "wasm"))] - ClientTransports::Udp => TransportConfig::UdpSocket(client_addr), - ClientTransports::WebTransport { certificate_digest } => { - TransportConfig::WebTransportClient { - client_addr, - server_addr, - #[cfg(target_family = "wasm")] - certificate_digest, - } - } - ClientTransports::WebSocket => TransportConfig::WebSocketClient { server_addr }, - }; let auth = Authentication::Manual { server_addr, client_id, diff --git a/examples/interest_management/src/main.rs b/examples/interest_management/src/main.rs index 4e7e505e..f75129e8 100644 --- a/examples/interest_management/src/main.rs +++ b/examples/interest_management/src/main.rs @@ -1,24 +1,15 @@ #![allow(unused_imports)] #![allow(unused_variables)] #![allow(dead_code)] -//! Example showcasing interest-management: only the entities that are in the same room as the player are sent to the client. -//! -//! This example is compatible with WASM. -//! + //! Run with //! - `cargo run -- server` //! - `cargo run -- client -c 1` -mod client; -mod protocol; -#[cfg(not(target_family = "wasm"))] -mod server; -mod shared; +use std::net::{Ipv4Addr, SocketAddr}; +use std::str::FromStr; use async_compat::Compat; use bevy::asset::ron; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::str::FromStr; - use bevy::log::{Level, LogPlugin}; use bevy::prelude::*; use bevy::tasks::IoTaskPool; @@ -27,16 +18,22 @@ use bevy_inspector_egui::quick::WorldInspectorPlugin; use clap::{Parser, ValueEnum}; use serde::{Deserialize, Serialize}; +use lightyear::connection::netcode::ClientId; +use lightyear::prelude::server::Certificate; +use lightyear::prelude::TransportConfig; +use lightyear::shared::log::add_log_layer; +use lightyear::transport::LOCAL_SOCKET; + use crate::client::ClientPluginGroup; #[cfg(not(target_family = "wasm"))] use crate::server::ServerPluginGroup; -use lightyear::connection::netcode::{ClientId, Key}; -use lightyear::prelude::TransportConfig; -use lightyear::shared::log::add_log_layer; -pub const PROTOCOL_ID: u64 = 0; +mod client; +mod protocol; -pub const KEY: Key = [0; 32]; +#[cfg(not(target_family = "wasm"))] +mod server; +mod shared; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum ClientTransports { @@ -55,7 +52,7 @@ pub enum ServerTransports { WebSocket { local_port: u16 }, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct ServerSettings { /// If true, disable any rendering-related plugins headless: bool, @@ -67,7 +64,7 @@ pub struct ServerSettings { transport: Vec, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct ClientSettings { /// If true, enable bevy_inspector_egui inspector: bool, @@ -97,7 +94,7 @@ pub struct SharedSettings { private_key: [u8; 32], } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Settings { pub server: ServerSettings, pub client: ClientSettings, @@ -107,7 +104,15 @@ pub struct Settings { #[derive(Parser, PartialEq, Debug)] enum Cli { #[cfg(not(target_family = "wasm"))] + /// The program will act both as a server and as a client + ListenServer { + #[arg(short, long, default_value = None)] + client_id: Option, + }, + #[cfg(not(target_family = "wasm"))] + /// Dedicated server Server, + /// The program will act as a client Client { #[arg(short, long, default_value = None)] client_id: Option, @@ -127,66 +132,157 @@ fn main() { } let settings_str = include_str!("../assets/settings.ron"); let settings = ron::de::from_str::(settings_str).unwrap(); - let mut app = App::new(); - setup(&mut app, settings, cli); - app.run(); + run(settings, cli); } -fn setup(app: &mut App, settings: Settings, cli: Cli) { +fn run(settings: Settings, cli: Cli) { match cli { + #[cfg(not(target_family = "wasm"))] + Cli::ListenServer { client_id } => { + // create client app + let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); + let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); + let transport_config = TransportConfig::LocalChannel { + recv: from_server_recv, + send: to_server_send, + }; + // when communicating via channels, we need to use the address `LOCAL_SOCKET` for the server + let mut client_app = + client_app(settings.clone(), LOCAL_SOCKET, client_id, transport_config); + + // create server app + let extra_transport_configs = vec![TransportConfig::Channels { + // even if we communicate via channels, we need to provide a socket address for the client + channels: vec![(LOCAL_SOCKET, to_server_recv, from_server_send)], + }]; + let mut server_app = server_app(settings, extra_transport_configs); + + // run both the client and server apps + std::thread::spawn(move || server_app.run()); + client_app.run(); + } #[cfg(not(target_family = "wasm"))] Cli::Server => { - let shared = settings.shared; - let settings = settings.server; - if !settings.headless { - app.add_plugins(DefaultPlugins.build().disable::()); - } else { - app.add_plugins(MinimalPlugins); - } - app.add_plugins(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), - update_subscriber: Some(add_log_layer), - }); - - if settings.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - // this is async because we need to load the certificate from io - // we need async_compat because wtransport expects a tokio reactor - let server_plugin_group = IoTaskPool::get() - .scope(|s| { - s.spawn(Compat::new(async { - ServerPluginGroup::new(settings.transport, shared).await - })); - }) - .pop() - .unwrap(); - app.add_plugins(server_plugin_group.build()); + let mut app = server_app(settings, vec![]); + app.run(); } Cli::Client { client_id } => { - let shared = settings.shared; - let settings = settings.client; - // NOTE: create the default plugins first so that the async task pools are initialized - // use the default bevy logger for now - // (the lightyear logger doesn't handle wasm) - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.inspector { - app.add_plugins(WorldInspectorPlugin::new()); + let server_addr = SocketAddr::new( + settings.client.server_addr.into(), + settings.client.server_port, + ); + let transport_config = get_client_transport_config(settings.client.clone()); + let mut app = client_app(settings, server_addr, client_id, transport_config); + app.run(); + } + } +} + +/// Build the client app +fn client_app( + settings: Settings, + server_addr: SocketAddr, + client_id: Option, + transport_config: TransportConfig, +) -> App { + let mut app = App::new(); + // NOTE: create the default plugins first so that the async task pools are initialized + // use the default bevy logger for now + // (the lightyear logger doesn't handle wasm) + app.add_plugins(DefaultPlugins.build().set(LogPlugin { + level: Level::INFO, + filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), + update_subscriber: Some(add_log_layer), + })); + if settings.client.inspector { + app.add_plugins(WorldInspectorPlugin::new()); + } + let client_plugin_group = ClientPluginGroup::new( + // use the cli-provided client id if it exists, otherwise use the settings client id + client_id.unwrap_or(settings.client.client_id), + server_addr, + transport_config, + settings.shared, + ); + app.add_plugins(client_plugin_group.build()); + app +} + +/// Build the server app +fn server_app(settings: Settings, extra_transport_configs: Vec) -> App { + let mut app = App::new(); + if !settings.server.headless { + app.add_plugins(DefaultPlugins.build().disable::()); + } else { + app.add_plugins(MinimalPlugins); + } + app.add_plugins(LogPlugin { + level: Level::INFO, + filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), + update_subscriber: Some(add_log_layer), + }); + + if settings.server.inspector { + app.add_plugins(WorldInspectorPlugin::new()); + } + let mut transport_configs = get_server_transport_configs(settings.server.transport); + transport_configs.extend(extra_transport_configs); + let server_plugin_group = ServerPluginGroup::new(transport_configs, settings.shared); + app.add_plugins(server_plugin_group.build()); + app +} + +/// Parse the server transport settings into a list of `TransportConfig` that are used to configure the lightyear server +fn get_server_transport_configs(settings: Vec) -> Vec { + settings + .iter() + .map(|t| match t { + ServerTransports::Udp { local_port } => TransportConfig::UdpSocket(SocketAddr::new( + Ipv4Addr::UNSPECIFIED.into(), + *local_port, + )), + ServerTransports::WebTransport { local_port } => { + // this is async because we need to load the certificate from io + // we need async_compat because wtransport expects a tokio reactor + let certificate = IoTaskPool::get() + .scope(|s| { + s.spawn(Compat::new(async { + Certificate::load("../certificates/cert.pem", "../certificates/key.pem") + .await + .unwrap() + })); + }) + .pop() + .unwrap(); + let digest = &certificate.hashes()[0].to_string().replace(":", ""); + println!("Generated self-signed certificate with digest: {}", digest); + TransportConfig::WebTransportServer { + server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), + certificate, + } } - let server_addr = SocketAddr::new(settings.server_addr.into(), settings.server_port); - let client_plugin_group = ClientPluginGroup::new( - client_id.unwrap_or(settings.client_id), - settings.client_port, + ServerTransports::WebSocket { local_port } => TransportConfig::WebSocketServer { + server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), + }, + }) + .collect() +} + +/// Parse the client transport settings into a `TransportConfig` that is used to configure the lightyear client +fn get_client_transport_config(settings: ClientSettings) -> TransportConfig { + let server_addr = SocketAddr::new(settings.server_addr.into(), settings.server_port); + let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), settings.client_port); + match settings.transport { + #[cfg(not(target_family = "wasm"))] + ClientTransports::Udp => TransportConfig::UdpSocket(client_addr), + ClientTransports::WebTransport { certificate_digest } => { + TransportConfig::WebTransportClient { + client_addr, server_addr, - settings.transport, - shared, - ); - app.add_plugins(client_plugin_group.build()); + #[cfg(target_family = "wasm")] + certificate_digest, + } } + ClientTransports::WebSocket => TransportConfig::WebSocketClient { server_addr }, } } diff --git a/examples/interest_management/src/protocol.rs b/examples/interest_management/src/protocol.rs index 589a706f..66384798 100644 --- a/examples/interest_management/src/protocol.rs +++ b/examples/interest_management/src/protocol.rs @@ -1,3 +1,5 @@ +use std::ops::Mul; + use bevy::math::Vec2; use bevy::prelude::*; use derive_more::{Add, Mul}; @@ -5,11 +7,11 @@ use leafwing_input_manager::action_state::ActionState; use leafwing_input_manager::input_map::InputMap; use leafwing_input_manager::prelude::Actionlike; use leafwing_input_manager::InputManagerBundle; -use lightyear::prelude::*; -use lightyear::shared::replication::components::ReplicationMode; use serde::{Deserialize, Serialize}; -use std::ops::Mul; use tracing::info; + +use lightyear::prelude::*; +use lightyear::shared::replication::components::ReplicationMode; use UserAction; // Player diff --git a/examples/interest_management/src/server.rs b/examples/interest_management/src/server.rs index 70c5efe8..6597a983 100644 --- a/examples/interest_management/src/server.rs +++ b/examples/interest_management/src/server.rs @@ -1,15 +1,18 @@ -use crate::protocol::*; -use crate::shared::{shared_config, shared_movement_behaviour}; -use crate::{shared, ServerTransports, SharedSettings, KEY, PROTOCOL_ID}; +use std::collections::HashMap; +use std::net::{Ipv4Addr, SocketAddr}; + use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use bevy::utils::Duration; use leafwing_input_manager::prelude::ActionState; + use lightyear::prelude::server::*; use lightyear::prelude::*; use lightyear::server::events::ServerEvents; -use std::collections::HashMap; -use std::net::{Ipv4Addr, SocketAddr}; + +use crate::protocol::*; +use crate::shared::{shared_config, shared_movement_behaviour}; +use crate::{shared, ServerTransports, SharedSettings}; const GRID_SIZE: f32 = 200.0; const NUM_CIRCLES: i32 = 10; @@ -24,8 +27,8 @@ pub struct ServerPluginGroup { } impl ServerPluginGroup { - pub(crate) async fn new( - transports: Vec, + pub(crate) fn new( + transport_configs: Vec, shared_settings: SharedSettings, ) -> ServerPluginGroup { // Step 1: create the io (transport + link conditioner) @@ -35,31 +38,7 @@ impl ServerPluginGroup { incoming_loss: 0.00, }; let mut net_configs = vec![]; - for transport in &transports { - let transport_config = match transport { - ServerTransports::Udp { local_port } => { - let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port); - TransportConfig::UdpSocket(server_addr) - } - // if using webtransport, we load the certificate keys - ServerTransports::WebTransport { local_port } => { - let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port); - let certificate = - Certificate::load("../certificates/cert.pem", "../certificates/key.pem") - .await - .unwrap(); - let digest = &certificate.hashes()[0].to_string().replace(":", ""); - println!("Generated self-signed certificate with digest: {}", digest); - TransportConfig::WebTransportServer { - server_addr, - certificate, - } - } - ServerTransports::WebSocket { local_port } => { - let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port); - TransportConfig::WebSocketServer { server_addr } - } - }; + for transport_config in transport_configs { net_configs.push(NetConfig::Netcode { config: NetcodeConfig::default() .with_protocol_id(shared_settings.protocol_id) diff --git a/examples/interest_management/src/shared.rs b/examples/interest_management/src/shared.rs index 00334a02..e3b03763 100644 --- a/examples/interest_management/src/shared.rs +++ b/examples/interest_management/src/shared.rs @@ -1,13 +1,13 @@ -use crate::protocol::*; -use bevy::prelude::*; +use std::ops::Deref; +use bevy::prelude::*; use bevy::render::RenderPlugin; use bevy::utils::Duration; use leafwing_input_manager::action_state::ActionState; -use lightyear::prelude::client::Confirmed; + use lightyear::prelude::*; -use std::ops::Deref; -use tracing::Level; + +use crate::protocol::*; pub fn shared_config() -> SharedConfig { SharedConfig { diff --git a/examples/leafwing_inputs/Cargo.toml b/examples/leafwing_inputs/Cargo.toml index 391cd96c..5f095777 100644 --- a/examples/leafwing_inputs/Cargo.toml +++ b/examples/leafwing_inputs/Cargo.toml @@ -35,3 +35,4 @@ mock_instant = "0.3" metrics-exporter-prometheus = { version = "0.13.0", optional = true } bevy-inspector-egui = "0.23" cfg-if = "1.0.0" +crossbeam-channel = "0.5.11" diff --git a/examples/leafwing_inputs/README.md b/examples/leafwing_inputs/README.md index 341f3672..dbd726cc 100644 --- a/examples/leafwing_inputs/README.md +++ b/examples/leafwing_inputs/README.md @@ -1,30 +1,57 @@ # Features - This example showcases several things: -- how to integrate lightyear with `leafwing_input_manager`. In particular you can simply attach an `ActionState` and an `InputMap` + +- how to integrate lightyear with `leafwing_input_manager`. In particular you can simply attach an `ActionState` and + an `InputMap` to an `Entity`, and the `ActionState` for that `Entity` will be replicated automatically -- an example of how to integrate physics replication with `bevy_xpbd`. The physics sets have to be run in `FixedUpdate` schedule -- an example of how to run prediction for entities that are controlled by other players. (this is similar to what RocketLeague does). - There is going to be a frequent number of mispredictions because the client is predicting other players without knowing their inputs. +- an example of how to integrate physics replication with `bevy_xpbd`. The physics sets have to be run in `FixedUpdate` + schedule +- an example of how to run prediction for entities that are controlled by other players. (this is similar to what + RocketLeague does). + There is going to be a frequent number of mispredictions because the client is predicting other players without + knowing their inputs. The client will just consider that other players are doing the same thing as the last time it received their inputs. - You can use the parameter `--predict` on the server to enable this behaviour (if not, other players will be interpolated). + You can use the parameter `--predict` on the server to enable this behaviour (if not, other players will be + interpolated). - The prediction behaviour can be adjusted by two parameters: - - `input_delay`: the number of frames it will take for an input to be executed. If the input delay is greater than the RTT, - there should be no mispredictions at all, but the game will feel more laggy. - - `correction_ticks`: when there is a misprediction, we don't immediately snapback to the corrected state, but instead we visually interpolate - from the current state to the corrected state. This parameter helps make mispredictions less jittery. - + - `input_delay`: the number of frames it will take for an input to be executed. If the input delay is greater than + the RTT, + there should be no mispredictions at all, but the game will feel more laggy. + - `correction_ticks`: when there is a misprediction, we don't immediately snapback to the corrected state, but + instead we visually interpolate + from the current state to the corrected state. This parameter helps make mispredictions less jittery. https://github.com/cBournhonesque/lightyear/assets/8112632/ac6fb465-26b8-4f5b-b22b-d79d0f48f7dd -*Example with 150ms of simulated RTT, a 32Hz server replication rate, 7 ticks of input-delay, and rollback-corrections enabled.* +*Example with 150ms of simulated RTT, a 32Hz server replication rate, 7 ticks of input-delay, and rollback-corrections +enabled.* + +## Running the example + +You can either run the example as a "Listen Server" (the program acts as both client and server) +with: `cargo run -- listen-server` +or as dedicated server with `cargo run -- server` + +Then you can launch multiple clients with the commands: + +- `cargo run -- client -c 1` +- `cargo run -- client -c 2` + +You can modify the file `assets/settings.ron` to modify some networking settings. + +### Testing in wasm with webtransport +NOTE: I am using [trunk](https://trunkrs.dev/) to build and serve the wasm example. +To test the example in wasm, you can run the following commands: `trunk serve` -# Usage +You will need a valid SSL certificate to test the example in wasm using webtransport. You will need to run the following +commands: -- Run the server with: `cargo run -- server --predict` -- Run the clients with: -`cargo run -- client -c 1` -`cargo run -- client -c 2` +- `sh examples/generate.sh` (to generate the temporary SSL certificates, they are only valid for 2 weeks) +- `cargo run -- server` to start the server. The server will print out the certificate digest (something + like `1fd28860bd2010067cee636a64bcbb492142295b297fd8c480e604b70ce4d644`) +- You then have to replace the certificate digest in the `assets/settings.ron` file with the one that the server printed + out. +- then start the client wasm test with `trunk serve` \ No newline at end of file diff --git a/examples/leafwing_inputs/src/client.rs b/examples/leafwing_inputs/src/client.rs index 1d3fd6d6..4888c2e8 100644 --- a/examples/leafwing_inputs/src/client.rs +++ b/examples/leafwing_inputs/src/client.rs @@ -1,6 +1,6 @@ -use crate::protocol::*; -use crate::shared::{color_from_id, shared_config, shared_movement_behaviour, FixedSet}; -use crate::{shared, ClientTransports, SharedSettings, KEY, PROTOCOL_ID}; +use std::net::{Ipv4Addr, SocketAddr}; +use std::str::FromStr; + use bevy::app::PluginGroupBuilder; use bevy::ecs::schedule::{LogLevel, ScheduleBuildSettings}; use bevy::prelude::*; @@ -8,12 +8,15 @@ use bevy::utils::Duration; use bevy_xpbd_2d::parry::shape::ShapeType::Ball; use bevy_xpbd_2d::prelude::*; use leafwing_input_manager::prelude::*; + use lightyear::inputs::native::input_buffer::InputBuffer; use lightyear::prelude::client::LeafwingInputPlugin; use lightyear::prelude::client::*; use lightyear::prelude::*; -use std::net::{Ipv4Addr, SocketAddr}; -use std::str::FromStr; + +use crate::protocol::*; +use crate::shared::{color_from_id, shared_config, shared_movement_behaviour, FixedSet}; +use crate::{shared, ClientTransports, SharedSettings}; pub const INPUT_DELAY_TICKS: u16 = 0; pub const CORRECTION_TICKS_FACTOR: f32 = 1.5; @@ -26,25 +29,10 @@ pub struct ClientPluginGroup { impl ClientPluginGroup { pub(crate) fn new( client_id: u64, - client_port: u16, server_addr: SocketAddr, - transport: ClientTransports, + transport_config: TransportConfig, shared_settings: SharedSettings, ) -> ClientPluginGroup { - let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), client_port); - let transport_config = match transport { - #[cfg(not(target_family = "wasm"))] - ClientTransports::Udp => TransportConfig::UdpSocket(client_addr), - ClientTransports::WebTransport { certificate_digest } => { - TransportConfig::WebTransportClient { - client_addr, - server_addr, - #[cfg(target_family = "wasm")] - certificate_digest, - } - } - ClientTransports::WebSocket => TransportConfig::WebSocketClient { server_addr }, - }; let auth = Authentication::Manual { server_addr, client_id, diff --git a/examples/leafwing_inputs/src/main.rs b/examples/leafwing_inputs/src/main.rs index 9dfa9dbc..c093dc4f 100644 --- a/examples/leafwing_inputs/src/main.rs +++ b/examples/leafwing_inputs/src/main.rs @@ -5,17 +5,11 @@ //! Run with //! - `cargo run -- server` //! - `cargo run -- client -c 1` -mod client; -mod protocol; -#[cfg(not(target_family = "wasm"))] -mod server; -mod shared; - -use async_compat::Compat; -use bevy::asset::ron; use std::net::{Ipv4Addr, SocketAddr}; use std::str::FromStr; +use async_compat::Compat; +use bevy::asset::ron; use bevy::log::{Level, LogPlugin}; use bevy::prelude::*; use bevy::tasks::IoTaskPool; @@ -24,16 +18,22 @@ use bevy_inspector_egui::quick::WorldInspectorPlugin; use clap::{Parser, ValueEnum}; use serde::{Deserialize, Serialize}; +use lightyear::connection::netcode::ClientId; +use lightyear::prelude::server::Certificate; +use lightyear::prelude::TransportConfig; +use lightyear::shared::log::add_log_layer; +use lightyear::transport::LOCAL_SOCKET; + use crate::client::ClientPluginGroup; #[cfg(not(target_family = "wasm"))] use crate::server::ServerPluginGroup; -use lightyear::connection::netcode::{ClientId, Key}; -use lightyear::prelude::TransportConfig; -use lightyear::shared::log::add_log_layer; -pub const PROTOCOL_ID: u64 = 0; +mod client; +mod protocol; -pub const KEY: Key = [0; 32]; +#[cfg(not(target_family = "wasm"))] +mod server; +mod shared; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum ClientTransports { @@ -52,7 +52,7 @@ pub enum ServerTransports { WebSocket { local_port: u16 }, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct ServerSettings { /// If true, disable any rendering-related plugins headless: bool, @@ -60,15 +60,14 @@ pub struct ServerSettings { /// If true, enable bevy_inspector_egui inspector: bool, - /// If true, we will run prediction for other clients as well - /// (otherwise we will simply run interpolation) + /// If true, apply prediction to all clients (even other clients) predict_all: bool, /// Which transport to use transport: Vec, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct ClientSettings { /// If true, enable bevy_inspector_egui inspector: bool, @@ -98,7 +97,7 @@ pub struct SharedSettings { private_key: [u8; 32], } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Settings { pub server: ServerSettings, pub client: ClientSettings, @@ -108,7 +107,15 @@ pub struct Settings { #[derive(Parser, PartialEq, Debug)] enum Cli { #[cfg(not(target_family = "wasm"))] + /// The program will act both as a server and as a client + ListenServer { + #[arg(short, long, default_value = None)] + client_id: Option, + }, + #[cfg(not(target_family = "wasm"))] + /// Dedicated server Server, + /// The program will act as a client Client { #[arg(short, long, default_value = None)] client_id: Option, @@ -128,67 +135,161 @@ fn main() { } let settings_str = include_str!("../assets/settings.ron"); let settings = ron::de::from_str::(settings_str).unwrap(); - let mut app = App::new(); - setup(&mut app, settings, cli); - app.run(); + run(settings, cli); } -fn setup(app: &mut App, settings: Settings, cli: Cli) { +fn run(settings: Settings, cli: Cli) { match cli { + #[cfg(not(target_family = "wasm"))] + Cli::ListenServer { client_id } => { + // create client app + let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); + let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); + let transport_config = TransportConfig::LocalChannel { + recv: from_server_recv, + send: to_server_send, + }; + // when communicating via channels, we need to use the address `LOCAL_SOCKET` for the server + let mut client_app = + client_app(settings.clone(), LOCAL_SOCKET, client_id, transport_config); + + // create server app + let extra_transport_configs = vec![TransportConfig::Channels { + // even if we communicate via channels, we need to provide a socket address for the client + channels: vec![(LOCAL_SOCKET, to_server_recv, from_server_send)], + }]; + let mut server_app = server_app(settings, extra_transport_configs); + + // run both the client and server apps + std::thread::spawn(move || server_app.run()); + client_app.run(); + } #[cfg(not(target_family = "wasm"))] Cli::Server => { - let shared = settings.shared; - let settings = settings.server; - if !settings.headless { - app.add_plugins(DefaultPlugins.build().disable::()); - } else { - app.add_plugins(MinimalPlugins); - } - app.add_plugins(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), - update_subscriber: Some(add_log_layer), - }); - - if settings.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - // this is async because we need to load the certificate from io - // we need async_compat because wtransport expects a tokio reactor - let server_plugin_group = IoTaskPool::get() - .scope(|s| { - s.spawn(Compat::new(async { - ServerPluginGroup::new(settings.transport, settings.predict_all, shared) - .await - })); - }) - .pop() - .unwrap(); - app.add_plugins(server_plugin_group.build()); + let mut app = server_app(settings, vec![]); + app.run(); } Cli::Client { client_id } => { - let shared = settings.shared; - let settings = settings.client; - // NOTE: create the default plugins first so that the async task pools are initialized - // use the default bevy logger for now - // (the lightyear logger doesn't handle wasm) - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::WARN, - filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.inspector { - app.add_plugins(WorldInspectorPlugin::new()); + let server_addr = SocketAddr::new( + settings.client.server_addr.into(), + settings.client.server_port, + ); + let transport_config = get_client_transport_config(settings.client.clone()); + let mut app = client_app(settings, server_addr, client_id, transport_config); + app.run(); + } + } +} + +/// Build the client app +fn client_app( + settings: Settings, + server_addr: SocketAddr, + client_id: Option, + transport_config: TransportConfig, +) -> App { + let mut app = App::new(); + // NOTE: create the default plugins first so that the async task pools are initialized + // use the default bevy logger for now + // (the lightyear logger doesn't handle wasm) + app.add_plugins(DefaultPlugins.build().set(LogPlugin { + level: Level::INFO, + filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), + update_subscriber: Some(add_log_layer), + })); + if settings.client.inspector { + app.add_plugins(WorldInspectorPlugin::new()); + } + let client_plugin_group = ClientPluginGroup::new( + // use the cli-provided client id if it exists, otherwise use the settings client id + client_id.unwrap_or(settings.client.client_id), + server_addr, + transport_config, + settings.shared, + ); + app.add_plugins(client_plugin_group.build()); + app +} + +/// Build the server app +fn server_app(settings: Settings, extra_transport_configs: Vec) -> App { + let mut app = App::new(); + if !settings.server.headless { + app.add_plugins(DefaultPlugins.build().disable::()); + } else { + app.add_plugins(MinimalPlugins); + } + app.add_plugins(LogPlugin { + level: Level::INFO, + filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), + update_subscriber: Some(add_log_layer), + }); + + if settings.server.inspector { + app.add_plugins(WorldInspectorPlugin::new()); + } + let mut transport_configs = get_server_transport_configs(settings.server.transport); + transport_configs.extend(extra_transport_configs); + let server_plugin_group = ServerPluginGroup::new( + transport_configs, + settings.server.predict_all, + settings.shared, + ); + app.add_plugins(server_plugin_group.build()); + app +} + +/// Parse the server transport settings into a list of `TransportConfig` that are used to configure the lightyear server +fn get_server_transport_configs(settings: Vec) -> Vec { + settings + .iter() + .map(|t| match t { + ServerTransports::Udp { local_port } => TransportConfig::UdpSocket(SocketAddr::new( + Ipv4Addr::UNSPECIFIED.into(), + *local_port, + )), + ServerTransports::WebTransport { local_port } => { + // this is async because we need to load the certificate from io + // we need async_compat because wtransport expects a tokio reactor + let certificate = IoTaskPool::get() + .scope(|s| { + s.spawn(Compat::new(async { + Certificate::load("../certificates/cert.pem", "../certificates/key.pem") + .await + .unwrap() + })); + }) + .pop() + .unwrap(); + let digest = &certificate.hashes()[0].to_string().replace(":", ""); + println!("Generated self-signed certificate with digest: {}", digest); + TransportConfig::WebTransportServer { + server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), + certificate, + } } - let server_addr = SocketAddr::new(settings.server_addr.into(), settings.server_port); - let client_plugin_group = ClientPluginGroup::new( - client_id.unwrap_or(settings.client_id), - settings.client_port, + ServerTransports::WebSocket { local_port } => TransportConfig::WebSocketServer { + server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), + }, + }) + .collect() +} + +/// Parse the client transport settings into a `TransportConfig` that is used to configure the lightyear client +fn get_client_transport_config(settings: ClientSettings) -> TransportConfig { + let server_addr = SocketAddr::new(settings.server_addr.into(), settings.server_port); + let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), settings.client_port); + match settings.transport { + #[cfg(not(target_family = "wasm"))] + ClientTransports::Udp => TransportConfig::UdpSocket(client_addr), + ClientTransports::WebTransport { certificate_digest } => { + TransportConfig::WebTransportClient { + client_addr, server_addr, - settings.transport, - shared, - ); - app.add_plugins(client_plugin_group.build()); + #[cfg(target_family = "wasm")] + certificate_digest, + } } + ClientTransports::WebSocket => TransportConfig::WebSocketClient { server_addr }, } } diff --git a/examples/leafwing_inputs/src/protocol.rs b/examples/leafwing_inputs/src/protocol.rs index c38ddd70..1a9963d7 100644 --- a/examples/leafwing_inputs/src/protocol.rs +++ b/examples/leafwing_inputs/src/protocol.rs @@ -2,10 +2,11 @@ use bevy::prelude::*; use bevy_xpbd_2d::prelude::*; use derive_more::{Add, Mul}; use leafwing_input_manager::prelude::*; +use serde::{Deserialize, Serialize}; + use lightyear::client::components::LerpFn; use lightyear::prelude::*; use lightyear::utils::bevy_xpbd_2d::*; -use serde::{Deserialize, Serialize}; pub const BALL_SIZE: f32 = 15.0; pub const PLAYER_SIZE: f32 = 40.0; diff --git a/examples/leafwing_inputs/src/server.rs b/examples/leafwing_inputs/src/server.rs index 1782d24b..85365b2b 100644 --- a/examples/leafwing_inputs/src/server.rs +++ b/examples/leafwing_inputs/src/server.rs @@ -1,15 +1,18 @@ -use crate::protocol::*; -use crate::shared::{color_from_id, shared_config, shared_movement_behaviour, FixedSet}; -use crate::{shared, ServerTransports, SharedSettings, KEY, PROTOCOL_ID}; +use std::collections::HashMap; +use std::net::{Ipv4Addr, SocketAddr}; + use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use bevy::utils::Duration; use bevy_xpbd_2d::prelude::*; use leafwing_input_manager::prelude::*; + use lightyear::prelude::server::*; use lightyear::prelude::*; -use std::collections::HashMap; -use std::net::{Ipv4Addr, SocketAddr}; + +use crate::protocol::*; +use crate::shared::{color_from_id, shared_config, shared_movement_behaviour, FixedSet}; +use crate::{shared, ServerTransports, SharedSettings}; // Plugin group to add all server-related plugins pub struct ServerPluginGroup { @@ -22,8 +25,8 @@ pub struct ServerPluginGroup { } impl ServerPluginGroup { - pub(crate) async fn new( - transports: Vec, + pub(crate) fn new( + transport_configs: Vec, predict_all: bool, shared_settings: SharedSettings, ) -> ServerPluginGroup { @@ -34,31 +37,7 @@ impl ServerPluginGroup { incoming_loss: 0.0, }; let mut net_configs = vec![]; - for transport in &transports { - let transport_config = match transport { - ServerTransports::Udp { local_port } => { - let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port); - TransportConfig::UdpSocket(server_addr) - } - // if using webtransport, we load the certificate keys - ServerTransports::WebTransport { local_port } => { - let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port); - let certificate = - Certificate::load("../certificates/cert.pem", "../certificates/key.pem") - .await - .unwrap(); - let digest = &certificate.hashes()[0].to_string().replace(":", ""); - println!("Generated self-signed certificate with digest: {}", digest); - TransportConfig::WebTransportServer { - server_addr, - certificate, - } - } - ServerTransports::WebSocket { local_port } => { - let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port); - TransportConfig::WebSocketServer { server_addr } - } - }; + for transport_config in transport_configs { net_configs.push(NetConfig::Netcode { config: NetcodeConfig::default() .with_protocol_id(shared_settings.protocol_id) diff --git a/examples/leafwing_inputs/src/shared.rs b/examples/leafwing_inputs/src/shared.rs index ab96ce31..fad582b2 100644 --- a/examples/leafwing_inputs/src/shared.rs +++ b/examples/leafwing_inputs/src/shared.rs @@ -1,4 +1,3 @@ -use crate::protocol::*; use bevy::diagnostic::LogDiagnosticsPlugin; use bevy::prelude::*; use bevy::render::RenderPlugin; @@ -8,12 +7,15 @@ use bevy_xpbd_2d::parry::shape::Ball; use bevy_xpbd_2d::prelude::*; use bevy_xpbd_2d::{PhysicsSchedule, PhysicsStepSet}; use leafwing_input_manager::prelude::ActionState; +use tracing::Level; + use lightyear::client::prediction::{Rollback, RollbackState}; use lightyear::prelude::client::*; use lightyear::prelude::TickManager; use lightyear::prelude::*; use lightyear::transport::io::IoDiagnosticsPlugin; -use tracing::Level; + +use crate::protocol::*; const FRAME_HZ: f64 = 60.0; const FIXED_TIMESTEP_HZ: f64 = 64.0; diff --git a/examples/priority/Cargo.toml b/examples/priority/Cargo.toml index f37f48d1..1e30441a 100644 --- a/examples/priority/Cargo.toml +++ b/examples/priority/Cargo.toml @@ -39,3 +39,4 @@ mock_instant = "0.3" metrics-exporter-prometheus = { version = "0.13.0", optional = true } bevy-inspector-egui = "0.23" cfg-if = "1.0.0" +crossbeam-channel = "0.5.11" diff --git a/examples/priority/README.md b/examples/priority/README.md index b7273e05..5cd8719b 100644 --- a/examples/priority/README.md +++ b/examples/priority/README.md @@ -1,29 +1,44 @@ # Priority A simple example that shows how you can specify which messages/channels/entities have priority over others. -In case the bandwidth quota is reached, lightyear will only send the messages with the highest priority, up to the quota. +In case the bandwidth quota is reached, lightyear will only send the messages with the highest priority, up to the +quota. To not starve lower priority entities, their priority is accumulated over time, so that they can eventually be sent. In this example, the center row has priority 1.0, and each row further away from the center has a priority of +1.0. (e.g. row 5 will get updated 5 times more frequently than row 1.0) -You can find more information in the [book](https://github.com/cBournhonesque/lightyear/blob/main/book/src/concepts/advanced_replication/bandwidth_management.md) - +You can find more information in +the [book](https://github.com/cBournhonesque/lightyear/blob/main/book/src/concepts/advanced_replication/bandwidth_management.md) https://github.com/cBournhonesque/lightyear/assets/8112632/0efcd974-b181-4910-9312-5307fbd45718 - - ## Running the example -To start the server, run `cargo run -- server` +You can either run the example as a "Listen Server" (the program acts as both client and server) +with: `cargo run -- listen-server` +or as dedicated server with `cargo run -- server` Then you can launch multiple clients with the commands: - `cargo run -- client -c 1` -- `cargo run -- client -c 2 --client-port 2000` +- `cargo run -- client -c 2` + +You can modify the file `assets/settings.ron` to modify some networking settings. + +### Testing in wasm with webtransport +NOTE: I am using [trunk](https://trunkrs.dev/) to build and serve the wasm example. +To test the example in wasm, you can run the following commands: `trunk serve` +You will need a valid SSL certificate to test the example in wasm using webtransport. You will need to run the following +commands: +- `sh examples/generate.sh` (to generate the temporary SSL certificates, they are only valid for 2 weeks) +- `cargo run -- server` to start the server. The server will print out the certificate digest (something + like `1fd28860bd2010067cee636a64bcbb492142295b297fd8c480e604b70ce4d644`) +- You then have to replace the certificate digest in the `assets/settings.ron` file with the one that the server printed + out. +- then start the client wasm test with `trunk serve` \ No newline at end of file diff --git a/examples/priority/src/client.rs b/examples/priority/src/client.rs index 4a540481..8512ea2c 100644 --- a/examples/priority/src/client.rs +++ b/examples/priority/src/client.rs @@ -1,17 +1,20 @@ -use crate::protocol::*; -use crate::shared::shared_config; -use crate::{shared, ClientTransports, SharedSettings, KEY, PROTOCOL_ID}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::str::FromStr; +use std::time::Duration; + use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use leafwing_input_manager::plugin::InputManagerSystem; use leafwing_input_manager::prelude::*; use leafwing_input_manager::systems::{run_if_enabled, tick_action_state}; + use lightyear::_reexport::ShouldBeInterpolated; use lightyear::prelude::client::*; use lightyear::prelude::*; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::str::FromStr; -use std::time::Duration; + +use crate::protocol::*; +use crate::shared::shared_config; +use crate::{shared, ClientTransports, SharedSettings}; pub struct ClientPluginGroup { lightyear: ClientPlugin, @@ -20,25 +23,10 @@ pub struct ClientPluginGroup { impl ClientPluginGroup { pub(crate) fn new( client_id: u64, - client_port: u16, server_addr: SocketAddr, - transport: ClientTransports, + transport_config: TransportConfig, shared_settings: SharedSettings, ) -> ClientPluginGroup { - let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), client_port); - let transport_config = match transport { - #[cfg(not(target_family = "wasm"))] - ClientTransports::Udp => TransportConfig::UdpSocket(client_addr), - ClientTransports::WebTransport { certificate_digest } => { - TransportConfig::WebTransportClient { - client_addr, - server_addr, - #[cfg(target_family = "wasm")] - certificate_digest, - } - } - ClientTransports::WebSocket => TransportConfig::WebSocketClient { server_addr }, - }; let auth = Authentication::Manual { server_addr, client_id, diff --git a/examples/priority/src/main.rs b/examples/priority/src/main.rs index c8635e8f..f75129e8 100644 --- a/examples/priority/src/main.rs +++ b/examples/priority/src/main.rs @@ -3,19 +3,13 @@ #![allow(dead_code)] //! Run with -//! - `cargo run --example interest_management -- server` -//! - `cargo run --example interest_management -- client -c 1` -mod client; -mod protocol; -#[cfg(not(target_family = "wasm"))] -mod server; -mod shared; - -use async_compat::Compat; -use bevy::asset::ron; +//! - `cargo run -- server` +//! - `cargo run -- client -c 1` use std::net::{Ipv4Addr, SocketAddr}; use std::str::FromStr; +use async_compat::Compat; +use bevy::asset::ron; use bevy::log::{Level, LogPlugin}; use bevy::prelude::*; use bevy::tasks::IoTaskPool; @@ -24,16 +18,22 @@ use bevy_inspector_egui::quick::WorldInspectorPlugin; use clap::{Parser, ValueEnum}; use serde::{Deserialize, Serialize}; +use lightyear::connection::netcode::ClientId; +use lightyear::prelude::server::Certificate; +use lightyear::prelude::TransportConfig; +use lightyear::shared::log::add_log_layer; +use lightyear::transport::LOCAL_SOCKET; + use crate::client::ClientPluginGroup; #[cfg(not(target_family = "wasm"))] use crate::server::ServerPluginGroup; -use lightyear::connection::netcode::{ClientId, Key}; -use lightyear::prelude::TransportConfig; -use lightyear::shared::log::add_log_layer; -pub const PROTOCOL_ID: u64 = 0; +mod client; +mod protocol; -pub const KEY: Key = [0; 32]; +#[cfg(not(target_family = "wasm"))] +mod server; +mod shared; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum ClientTransports { @@ -52,7 +52,7 @@ pub enum ServerTransports { WebSocket { local_port: u16 }, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct ServerSettings { /// If true, disable any rendering-related plugins headless: bool, @@ -64,7 +64,7 @@ pub struct ServerSettings { transport: Vec, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct ClientSettings { /// If true, enable bevy_inspector_egui inspector: bool, @@ -94,7 +94,7 @@ pub struct SharedSettings { private_key: [u8; 32], } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Settings { pub server: ServerSettings, pub client: ClientSettings, @@ -104,7 +104,15 @@ pub struct Settings { #[derive(Parser, PartialEq, Debug)] enum Cli { #[cfg(not(target_family = "wasm"))] + /// The program will act both as a server and as a client + ListenServer { + #[arg(short, long, default_value = None)] + client_id: Option, + }, + #[cfg(not(target_family = "wasm"))] + /// Dedicated server Server, + /// The program will act as a client Client { #[arg(short, long, default_value = None)] client_id: Option, @@ -124,66 +132,157 @@ fn main() { } let settings_str = include_str!("../assets/settings.ron"); let settings = ron::de::from_str::(settings_str).unwrap(); - let mut app = App::new(); - setup(&mut app, settings, cli); - app.run(); + run(settings, cli); } -fn setup(app: &mut App, settings: Settings, cli: Cli) { +fn run(settings: Settings, cli: Cli) { match cli { + #[cfg(not(target_family = "wasm"))] + Cli::ListenServer { client_id } => { + // create client app + let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); + let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); + let transport_config = TransportConfig::LocalChannel { + recv: from_server_recv, + send: to_server_send, + }; + // when communicating via channels, we need to use the address `LOCAL_SOCKET` for the server + let mut client_app = + client_app(settings.clone(), LOCAL_SOCKET, client_id, transport_config); + + // create server app + let extra_transport_configs = vec![TransportConfig::Channels { + // even if we communicate via channels, we need to provide a socket address for the client + channels: vec![(LOCAL_SOCKET, to_server_recv, from_server_send)], + }]; + let mut server_app = server_app(settings, extra_transport_configs); + + // run both the client and server apps + std::thread::spawn(move || server_app.run()); + client_app.run(); + } #[cfg(not(target_family = "wasm"))] Cli::Server => { - let shared = settings.shared; - let settings = settings.server; - if !settings.headless { - app.add_plugins(DefaultPlugins.build().disable::()); - } else { - app.add_plugins(MinimalPlugins); - } - app.add_plugins(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), - update_subscriber: Some(add_log_layer), - }); - - if settings.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - // this is async because we need to load the certificate from io - // we need async_compat because wtransport expects a tokio reactor - let server_plugin_group = IoTaskPool::get() - .scope(|s| { - s.spawn(Compat::new(async { - ServerPluginGroup::new(settings.transport, shared).await - })); - }) - .pop() - .unwrap(); - app.add_plugins(server_plugin_group.build()); + let mut app = server_app(settings, vec![]); + app.run(); } Cli::Client { client_id } => { - let shared = settings.shared; - let settings = settings.client; - // NOTE: create the default plugins first so that the async task pools are initialized - // use the default bevy logger for now - // (the lightyear logger doesn't handle wasm) - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.inspector { - app.add_plugins(WorldInspectorPlugin::new()); + let server_addr = SocketAddr::new( + settings.client.server_addr.into(), + settings.client.server_port, + ); + let transport_config = get_client_transport_config(settings.client.clone()); + let mut app = client_app(settings, server_addr, client_id, transport_config); + app.run(); + } + } +} + +/// Build the client app +fn client_app( + settings: Settings, + server_addr: SocketAddr, + client_id: Option, + transport_config: TransportConfig, +) -> App { + let mut app = App::new(); + // NOTE: create the default plugins first so that the async task pools are initialized + // use the default bevy logger for now + // (the lightyear logger doesn't handle wasm) + app.add_plugins(DefaultPlugins.build().set(LogPlugin { + level: Level::INFO, + filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), + update_subscriber: Some(add_log_layer), + })); + if settings.client.inspector { + app.add_plugins(WorldInspectorPlugin::new()); + } + let client_plugin_group = ClientPluginGroup::new( + // use the cli-provided client id if it exists, otherwise use the settings client id + client_id.unwrap_or(settings.client.client_id), + server_addr, + transport_config, + settings.shared, + ); + app.add_plugins(client_plugin_group.build()); + app +} + +/// Build the server app +fn server_app(settings: Settings, extra_transport_configs: Vec) -> App { + let mut app = App::new(); + if !settings.server.headless { + app.add_plugins(DefaultPlugins.build().disable::()); + } else { + app.add_plugins(MinimalPlugins); + } + app.add_plugins(LogPlugin { + level: Level::INFO, + filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), + update_subscriber: Some(add_log_layer), + }); + + if settings.server.inspector { + app.add_plugins(WorldInspectorPlugin::new()); + } + let mut transport_configs = get_server_transport_configs(settings.server.transport); + transport_configs.extend(extra_transport_configs); + let server_plugin_group = ServerPluginGroup::new(transport_configs, settings.shared); + app.add_plugins(server_plugin_group.build()); + app +} + +/// Parse the server transport settings into a list of `TransportConfig` that are used to configure the lightyear server +fn get_server_transport_configs(settings: Vec) -> Vec { + settings + .iter() + .map(|t| match t { + ServerTransports::Udp { local_port } => TransportConfig::UdpSocket(SocketAddr::new( + Ipv4Addr::UNSPECIFIED.into(), + *local_port, + )), + ServerTransports::WebTransport { local_port } => { + // this is async because we need to load the certificate from io + // we need async_compat because wtransport expects a tokio reactor + let certificate = IoTaskPool::get() + .scope(|s| { + s.spawn(Compat::new(async { + Certificate::load("../certificates/cert.pem", "../certificates/key.pem") + .await + .unwrap() + })); + }) + .pop() + .unwrap(); + let digest = &certificate.hashes()[0].to_string().replace(":", ""); + println!("Generated self-signed certificate with digest: {}", digest); + TransportConfig::WebTransportServer { + server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), + certificate, + } } - let server_addr = SocketAddr::new(settings.server_addr.into(), settings.server_port); - let client_plugin_group = ClientPluginGroup::new( - client_id.unwrap_or(settings.client_id), - settings.client_port, + ServerTransports::WebSocket { local_port } => TransportConfig::WebSocketServer { + server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), + }, + }) + .collect() +} + +/// Parse the client transport settings into a `TransportConfig` that is used to configure the lightyear client +fn get_client_transport_config(settings: ClientSettings) -> TransportConfig { + let server_addr = SocketAddr::new(settings.server_addr.into(), settings.server_port); + let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), settings.client_port); + match settings.transport { + #[cfg(not(target_family = "wasm"))] + ClientTransports::Udp => TransportConfig::UdpSocket(client_addr), + ClientTransports::WebTransport { certificate_digest } => { + TransportConfig::WebTransportClient { + client_addr, server_addr, - settings.transport, - shared, - ); - app.add_plugins(client_plugin_group.build()); + #[cfg(target_family = "wasm")] + certificate_digest, + } } + ClientTransports::WebSocket => TransportConfig::WebSocketClient { server_addr }, } } diff --git a/examples/priority/src/protocol.rs b/examples/priority/src/protocol.rs index b9d3408c..9018fc04 100644 --- a/examples/priority/src/protocol.rs +++ b/examples/priority/src/protocol.rs @@ -1,14 +1,16 @@ +use std::ops::Mul; + use bevy::prelude::*; use derive_more::{Add, Mul}; use leafwing_input_manager::action_state::ActionState; use leafwing_input_manager::input_map::InputMap; use leafwing_input_manager::prelude::Actionlike; use leafwing_input_manager::InputManagerBundle; -use lightyear::prelude::*; -use lightyear::shared::replication::components::ReplicationMode; use serde::{Deserialize, Serialize}; -use std::ops::Mul; use tracing::info; + +use lightyear::prelude::*; +use lightyear::shared::replication::components::ReplicationMode; use UserAction; // Player diff --git a/examples/priority/src/server.rs b/examples/priority/src/server.rs index ebbbd2d4..41c1a09f 100644 --- a/examples/priority/src/server.rs +++ b/examples/priority/src/server.rs @@ -1,16 +1,19 @@ -use crate::protocol::*; -use crate::shared::shared_config; -use crate::{shared, ServerTransports, SharedSettings, KEY, PROTOCOL_ID}; +use std::collections::HashMap; +use std::net::{Ipv4Addr, SocketAddr}; +use std::ops::Deref; +use std::time::Duration; + use bevy::app::PluginGroupBuilder; use bevy::ecs::archetype::Archetype; use bevy::prelude::*; use leafwing_input_manager::prelude::ActionState; + use lightyear::prelude::server::*; use lightyear::prelude::*; -use std::collections::HashMap; -use std::net::{Ipv4Addr, SocketAddr}; -use std::ops::Deref; -use std::time::Duration; + +use crate::protocol::*; +use crate::shared::shared_config; +use crate::{shared, ServerTransports, SharedSettings}; // Plugin group to add all server-related plugins pub struct ServerPluginGroup { @@ -18,8 +21,8 @@ pub struct ServerPluginGroup { } impl ServerPluginGroup { - pub(crate) async fn new( - transports: Vec, + pub(crate) fn new( + transport_configs: Vec, shared_settings: SharedSettings, ) -> ServerPluginGroup { // Step 1: create the io (transport + link conditioner) @@ -29,31 +32,7 @@ impl ServerPluginGroup { incoming_loss: 0.0, }; let mut net_configs = vec![]; - for transport in &transports { - let transport_config = match transport { - ServerTransports::Udp { local_port } => { - let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port); - TransportConfig::UdpSocket(server_addr) - } - // if using webtransport, we load the certificate keys - ServerTransports::WebTransport { local_port } => { - let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port); - let certificate = - Certificate::load("../certificates/cert.pem", "../certificates/key.pem") - .await - .unwrap(); - let digest = &certificate.hashes()[0].to_string().replace(":", ""); - println!("Generated self-signed certificate with digest: {}", digest); - TransportConfig::WebTransportServer { - server_addr, - certificate, - } - } - ServerTransports::WebSocket { local_port } => { - let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port); - TransportConfig::WebSocketServer { server_addr } - } - }; + for transport_config in transport_configs { net_configs.push(NetConfig::Netcode { config: NetcodeConfig::default() .with_protocol_id(shared_settings.protocol_id) diff --git a/examples/priority/src/shared.rs b/examples/priority/src/shared.rs index 6e6dff8b..b8c8d748 100644 --- a/examples/priority/src/shared.rs +++ b/examples/priority/src/shared.rs @@ -1,15 +1,16 @@ -use crate::protocol::*; -use bevy::prelude::*; +use std::ops::Deref; +use std::time::Duration; +use bevy::prelude::*; use bevy::render::RenderPlugin; use bevy_screen_diagnostics::{Aggregate, ScreenDiagnostics, ScreenDiagnosticsPlugin}; use leafwing_input_manager::action_state::ActionState; + use lightyear::prelude::client::Confirmed; use lightyear::prelude::*; use lightyear::transport::io::IoDiagnosticsPlugin; -use std::ops::Deref; -use std::time::Duration; -use tracing::Level; + +use crate::protocol::*; const MOVE_SPEED: f32 = 10.0; const PROP_SIZE: f32 = 5.0; diff --git a/examples/replication_groups/Cargo.toml b/examples/replication_groups/Cargo.toml index 5eec2c6b..763cb2b9 100644 --- a/examples/replication_groups/Cargo.toml +++ b/examples/replication_groups/Cargo.toml @@ -36,3 +36,4 @@ mock_instant = "0.3" metrics-exporter-prometheus = { version = "0.13.0", optional = true } bevy-inspector-egui = "0.23" cfg-if = "1.0.0" +crossbeam-channel = "0.5.11" diff --git a/examples/replication_groups/README.md b/examples/replication_groups/README.md index 37a80755..d5354d43 100644 --- a/examples/replication_groups/README.md +++ b/examples/replication_groups/README.md @@ -1,10 +1,12 @@ # Replication groups This is an example that shows how to make Lightyear replicate multiple entities in a single message, -to make sure that they are always in a consistent state (i.e. that entities in a group are all replicated on the same tick). +to make sure that they are always in a consistent state (i.e. that entities in a group are all replicated on the same +tick). Without a replication group, it is possible that one entity is replicated with the server's tick 10, and another entity -is replicated with the server's tick 11. This is not a problem if the entities are independent, but if they depend on each other (for example +is replicated with the server's tick 11. This is not a problem if the entities are independent, but if they depend on +each other (for example for client prediction) it could cause issues. This is especially useful if you have an entity that depends on another entity (e.g. a player and its weapon), @@ -12,17 +14,33 @@ the weapon might have a component `Parent(owner: Entity)` which references the p In which case we **need** the player entity to be spawned before the weapon entity, otherwise `Parent` component will reference an entity that does not exist. - https://github.com/cBournhonesque/lightyear/assets/8112632/e7625286-a167-4f50-aa52-9175cc168287 - - ## Running the example -To start the server, run `cargo run -- server` +You can either run the example as a "Listen Server" (the program acts as both client and server) +with: `cargo run -- listen-server` +or as dedicated server with `cargo run -- server` Then you can launch multiple clients with the commands: - `cargo run -- client -c 1` +- `cargo run -- client -c 2` + +You can modify the file `assets/settings.ron` to modify some networking settings. + +### Testing in wasm with webtransport + +NOTE: I am using [trunk](https://trunkrs.dev/) to build and serve the wasm example. + +To test the example in wasm, you can run the following commands: `trunk serve` + +You will need a valid SSL certificate to test the example in wasm using webtransport. You will need to run the following +commands: -- `cargo run -- client -c 2 --client-port 2000` +- `sh examples/generate.sh` (to generate the temporary SSL certificates, they are only valid for 2 weeks) +- `cargo run -- server` to start the server. The server will print out the certificate digest (something + like `1fd28860bd2010067cee636a64bcbb492142295b297fd8c480e604b70ce4d644`) +- You then have to replace the certificate digest in the `assets/settings.ron` file with the one that the server printed + out. +- then start the client wasm test with `trunk serve` diff --git a/examples/replication_groups/src/client.rs b/examples/replication_groups/src/client.rs index 5138ed9b..d4fa2493 100644 --- a/examples/replication_groups/src/client.rs +++ b/examples/replication_groups/src/client.rs @@ -1,17 +1,20 @@ -use crate::protocol::Direction; -use crate::protocol::*; -use crate::shared::{shared_config, shared_movement_behaviour, shared_tail_behaviour}; -use crate::{shared, ClientTransports, SharedSettings, KEY, PROTOCOL_ID}; +use std::collections::VecDeque; +use std::net::{Ipv4Addr, SocketAddr}; +use std::str::FromStr; + use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use bevy::utils::Duration; + use lightyear::_reexport::LinearInterpolator; use lightyear::connection::netcode::NetcodeServer; use lightyear::prelude::client::*; use lightyear::prelude::*; -use std::collections::VecDeque; -use std::net::{Ipv4Addr, SocketAddr}; -use std::str::FromStr; + +use crate::protocol::Direction; +use crate::protocol::*; +use crate::shared::{shared_config, shared_movement_behaviour, shared_tail_behaviour}; +use crate::{shared, ClientTransports, SharedSettings}; pub struct ClientPluginGroup { lightyear: ClientPlugin, @@ -20,25 +23,10 @@ pub struct ClientPluginGroup { impl ClientPluginGroup { pub(crate) fn new( client_id: u64, - client_port: u16, server_addr: SocketAddr, - transport: ClientTransports, + transport_config: TransportConfig, shared_settings: SharedSettings, ) -> ClientPluginGroup { - let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), client_port); - let transport_config = match transport { - #[cfg(not(target_family = "wasm"))] - ClientTransports::Udp => TransportConfig::UdpSocket(client_addr), - ClientTransports::WebTransport { certificate_digest } => { - TransportConfig::WebTransportClient { - client_addr, - server_addr, - #[cfg(target_family = "wasm")] - certificate_digest, - } - } - ClientTransports::WebSocket => TransportConfig::WebSocketClient { server_addr }, - }; let auth = Authentication::Manual { server_addr, client_id, diff --git a/examples/replication_groups/src/main.rs b/examples/replication_groups/src/main.rs index 8eece55c..f75129e8 100644 --- a/examples/replication_groups/src/main.rs +++ b/examples/replication_groups/src/main.rs @@ -5,17 +5,11 @@ //! Run with //! - `cargo run -- server` //! - `cargo run -- client -c 1` -mod client; -mod protocol; -#[cfg(not(target_family = "wasm"))] -mod server; -mod shared; - -use async_compat::Compat; -use bevy::asset::ron; use std::net::{Ipv4Addr, SocketAddr}; use std::str::FromStr; +use async_compat::Compat; +use bevy::asset::ron; use bevy::log::{Level, LogPlugin}; use bevy::prelude::*; use bevy::tasks::IoTaskPool; @@ -24,16 +18,22 @@ use bevy_inspector_egui::quick::WorldInspectorPlugin; use clap::{Parser, ValueEnum}; use serde::{Deserialize, Serialize}; +use lightyear::connection::netcode::ClientId; +use lightyear::prelude::server::Certificate; +use lightyear::prelude::TransportConfig; +use lightyear::shared::log::add_log_layer; +use lightyear::transport::LOCAL_SOCKET; + use crate::client::ClientPluginGroup; #[cfg(not(target_family = "wasm"))] use crate::server::ServerPluginGroup; -use lightyear::connection::netcode::{ClientId, Key}; -use lightyear::prelude::TransportConfig; -use lightyear::shared::log::add_log_layer; -pub const PROTOCOL_ID: u64 = 0; +mod client; +mod protocol; -pub const KEY: Key = [0; 32]; +#[cfg(not(target_family = "wasm"))] +mod server; +mod shared; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum ClientTransports { @@ -52,7 +52,7 @@ pub enum ServerTransports { WebSocket { local_port: u16 }, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct ServerSettings { /// If true, disable any rendering-related plugins headless: bool, @@ -64,7 +64,7 @@ pub struct ServerSettings { transport: Vec, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct ClientSettings { /// If true, enable bevy_inspector_egui inspector: bool, @@ -94,7 +94,7 @@ pub struct SharedSettings { private_key: [u8; 32], } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Settings { pub server: ServerSettings, pub client: ClientSettings, @@ -104,7 +104,15 @@ pub struct Settings { #[derive(Parser, PartialEq, Debug)] enum Cli { #[cfg(not(target_family = "wasm"))] + /// The program will act both as a server and as a client + ListenServer { + #[arg(short, long, default_value = None)] + client_id: Option, + }, + #[cfg(not(target_family = "wasm"))] + /// Dedicated server Server, + /// The program will act as a client Client { #[arg(short, long, default_value = None)] client_id: Option, @@ -124,66 +132,157 @@ fn main() { } let settings_str = include_str!("../assets/settings.ron"); let settings = ron::de::from_str::(settings_str).unwrap(); - let mut app = App::new(); - setup(&mut app, settings, cli); - app.run(); + run(settings, cli); } -fn setup(app: &mut App, settings: Settings, cli: Cli) { +fn run(settings: Settings, cli: Cli) { match cli { + #[cfg(not(target_family = "wasm"))] + Cli::ListenServer { client_id } => { + // create client app + let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); + let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); + let transport_config = TransportConfig::LocalChannel { + recv: from_server_recv, + send: to_server_send, + }; + // when communicating via channels, we need to use the address `LOCAL_SOCKET` for the server + let mut client_app = + client_app(settings.clone(), LOCAL_SOCKET, client_id, transport_config); + + // create server app + let extra_transport_configs = vec![TransportConfig::Channels { + // even if we communicate via channels, we need to provide a socket address for the client + channels: vec![(LOCAL_SOCKET, to_server_recv, from_server_send)], + }]; + let mut server_app = server_app(settings, extra_transport_configs); + + // run both the client and server apps + std::thread::spawn(move || server_app.run()); + client_app.run(); + } #[cfg(not(target_family = "wasm"))] Cli::Server => { - let shared = settings.shared; - let settings = settings.server; - if !settings.headless { - app.add_plugins(DefaultPlugins.build().disable::()); - } else { - app.add_plugins(MinimalPlugins); - } - app.add_plugins(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), - update_subscriber: Some(add_log_layer), - }); - - if settings.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - // this is async because we need to load the certificate from io - // we need async_compat because wtransport expects a tokio reactor - let server_plugin_group = IoTaskPool::get() - .scope(|s| { - s.spawn(Compat::new(async { - ServerPluginGroup::new(settings.transport, shared).await - })); - }) - .pop() - .unwrap(); - app.add_plugins(server_plugin_group.build()); + let mut app = server_app(settings, vec![]); + app.run(); } Cli::Client { client_id } => { - let shared = settings.shared; - let settings = settings.client; - // NOTE: create the default plugins first so that the async task pools are initialized - // use the default bevy logger for now - // (the lightyear logger doesn't handle wasm) - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.inspector { - app.add_plugins(WorldInspectorPlugin::new()); + let server_addr = SocketAddr::new( + settings.client.server_addr.into(), + settings.client.server_port, + ); + let transport_config = get_client_transport_config(settings.client.clone()); + let mut app = client_app(settings, server_addr, client_id, transport_config); + app.run(); + } + } +} + +/// Build the client app +fn client_app( + settings: Settings, + server_addr: SocketAddr, + client_id: Option, + transport_config: TransportConfig, +) -> App { + let mut app = App::new(); + // NOTE: create the default plugins first so that the async task pools are initialized + // use the default bevy logger for now + // (the lightyear logger doesn't handle wasm) + app.add_plugins(DefaultPlugins.build().set(LogPlugin { + level: Level::INFO, + filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), + update_subscriber: Some(add_log_layer), + })); + if settings.client.inspector { + app.add_plugins(WorldInspectorPlugin::new()); + } + let client_plugin_group = ClientPluginGroup::new( + // use the cli-provided client id if it exists, otherwise use the settings client id + client_id.unwrap_or(settings.client.client_id), + server_addr, + transport_config, + settings.shared, + ); + app.add_plugins(client_plugin_group.build()); + app +} + +/// Build the server app +fn server_app(settings: Settings, extra_transport_configs: Vec) -> App { + let mut app = App::new(); + if !settings.server.headless { + app.add_plugins(DefaultPlugins.build().disable::()); + } else { + app.add_plugins(MinimalPlugins); + } + app.add_plugins(LogPlugin { + level: Level::INFO, + filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), + update_subscriber: Some(add_log_layer), + }); + + if settings.server.inspector { + app.add_plugins(WorldInspectorPlugin::new()); + } + let mut transport_configs = get_server_transport_configs(settings.server.transport); + transport_configs.extend(extra_transport_configs); + let server_plugin_group = ServerPluginGroup::new(transport_configs, settings.shared); + app.add_plugins(server_plugin_group.build()); + app +} + +/// Parse the server transport settings into a list of `TransportConfig` that are used to configure the lightyear server +fn get_server_transport_configs(settings: Vec) -> Vec { + settings + .iter() + .map(|t| match t { + ServerTransports::Udp { local_port } => TransportConfig::UdpSocket(SocketAddr::new( + Ipv4Addr::UNSPECIFIED.into(), + *local_port, + )), + ServerTransports::WebTransport { local_port } => { + // this is async because we need to load the certificate from io + // we need async_compat because wtransport expects a tokio reactor + let certificate = IoTaskPool::get() + .scope(|s| { + s.spawn(Compat::new(async { + Certificate::load("../certificates/cert.pem", "../certificates/key.pem") + .await + .unwrap() + })); + }) + .pop() + .unwrap(); + let digest = &certificate.hashes()[0].to_string().replace(":", ""); + println!("Generated self-signed certificate with digest: {}", digest); + TransportConfig::WebTransportServer { + server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), + certificate, + } } - let server_addr = SocketAddr::new(settings.server_addr.into(), settings.server_port); - let client_plugin_group = ClientPluginGroup::new( - client_id.unwrap_or(settings.client_id), - settings.client_port, + ServerTransports::WebSocket { local_port } => TransportConfig::WebSocketServer { + server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), + }, + }) + .collect() +} + +/// Parse the client transport settings into a `TransportConfig` that is used to configure the lightyear client +fn get_client_transport_config(settings: ClientSettings) -> TransportConfig { + let server_addr = SocketAddr::new(settings.server_addr.into(), settings.server_port); + let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), settings.client_port); + match settings.transport { + #[cfg(not(target_family = "wasm"))] + ClientTransports::Udp => TransportConfig::UdpSocket(client_addr), + ClientTransports::WebTransport { certificate_digest } => { + TransportConfig::WebTransportClient { + client_addr, server_addr, - settings.transport, - shared, - ); - app.add_plugins(client_plugin_group.build()); + #[cfg(target_family = "wasm")] + certificate_digest, + } } + ClientTransports::WebSocket => TransportConfig::WebSocketClient { server_addr }, } } diff --git a/examples/replication_groups/src/protocol.rs b/examples/replication_groups/src/protocol.rs index 4e7c8a0e..ce842ea4 100644 --- a/examples/replication_groups/src/protocol.rs +++ b/examples/replication_groups/src/protocol.rs @@ -1,14 +1,16 @@ +use std::collections::VecDeque; +use std::ops::Mul; + use bevy::prelude::{ default, Bundle, Color, Component, Deref, DerefMut, Entity, EntityMapper, Reflect, Vec2, }; use derive_more::{Add, Mul}; +use serde::{Deserialize, Serialize}; +use tracing::{debug, info, trace}; + use lightyear::prelude::client::LerpFn; use lightyear::prelude::*; use lightyear::shared::replication::components::ReplicationGroup; -use serde::{Deserialize, Serialize}; -use std::collections::VecDeque; -use std::ops::Mul; -use tracing::{debug, info, trace}; // Player #[derive(Bundle)] diff --git a/examples/replication_groups/src/server.rs b/examples/replication_groups/src/server.rs index ed519da1..0611f380 100644 --- a/examples/replication_groups/src/server.rs +++ b/examples/replication_groups/src/server.rs @@ -1,13 +1,16 @@ -use crate::protocol::*; -use crate::shared::{shared_config, shared_movement_behaviour, shared_tail_behaviour}; -use crate::{shared, ServerTransports, SharedSettings, KEY, PROTOCOL_ID}; +use std::collections::HashMap; +use std::net::{Ipv4Addr, SocketAddr}; + use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use bevy::utils::Duration; + use lightyear::prelude::server::*; use lightyear::prelude::*; -use std::collections::HashMap; -use std::net::{Ipv4Addr, SocketAddr}; + +use crate::protocol::*; +use crate::shared::{shared_config, shared_movement_behaviour, shared_tail_behaviour}; +use crate::{shared, ServerTransports, SharedSettings}; // Plugin group to add all server-related plugins pub struct ServerPluginGroup { @@ -15,8 +18,8 @@ pub struct ServerPluginGroup { } impl ServerPluginGroup { - pub(crate) async fn new( - transports: Vec, + pub(crate) fn new( + transport_configs: Vec, shared_settings: SharedSettings, ) -> ServerPluginGroup { // Step 1: create the io (transport + link conditioner) @@ -26,31 +29,7 @@ impl ServerPluginGroup { incoming_loss: 0.05, }; let mut net_configs = vec![]; - for transport in &transports { - let transport_config = match transport { - ServerTransports::Udp { local_port } => { - let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port); - TransportConfig::UdpSocket(server_addr) - } - // if using webtransport, we load the certificate keys - ServerTransports::WebTransport { local_port } => { - let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port); - let certificate = - Certificate::load("../certificates/cert.pem", "../certificates/key.pem") - .await - .unwrap(); - let digest = &certificate.hashes()[0].to_string().replace(":", ""); - println!("Generated self-signed certificate with digest: {}", digest); - TransportConfig::WebTransportServer { - server_addr, - certificate, - } - } - ServerTransports::WebSocket { local_port } => { - let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port); - TransportConfig::WebSocketServer { server_addr } - } - }; + for transport_config in transport_configs { net_configs.push(NetConfig::Netcode { config: NetcodeConfig::default() .with_protocol_id(shared_settings.protocol_id) diff --git a/examples/replication_groups/src/shared.rs b/examples/replication_groups/src/shared.rs index 9885a335..d0cb9902 100644 --- a/examples/replication_groups/src/shared.rs +++ b/examples/replication_groups/src/shared.rs @@ -1,11 +1,13 @@ -use crate::protocol::Direction; -use crate::protocol::*; use bevy::prelude::*; use bevy::render::RenderPlugin; use bevy::utils::Duration; +use tracing::Level; + use lightyear::prelude::client::{Confirmed, Interpolated}; use lightyear::prelude::*; -use tracing::Level; + +use crate::protocol::Direction; +use crate::protocol::*; pub fn shared_config() -> SharedConfig { SharedConfig { diff --git a/examples/simple_box/Cargo.toml b/examples/simple_box/Cargo.toml index 836e9bd2..70f93d1f 100644 --- a/examples/simple_box/Cargo.toml +++ b/examples/simple_box/Cargo.toml @@ -35,3 +35,4 @@ mock_instant = "0.3" metrics-exporter-prometheus = { version = "0.13.0", optional = true } bevy-inspector-egui = "0.23" cfg-if = "1.0.0" +crossbeam-channel = "0.5.11" diff --git a/examples/simple_box/README.md b/examples/simple_box/README.md index 70c9cefe..cef33da5 100644 --- a/examples/simple_box/README.md +++ b/examples/simple_box/README.md @@ -8,14 +8,29 @@ https://github.com/cBournhonesque/lightyear/assets/8112632/7b57d48a-d8b0-4cdd-a1 ## Running the example -To start the server, run `cargo run -- server` +You can either run the example as a "Listen Server" (the program acts as both client and server) +with: `cargo run -- listen-server` +or as dedicated server with `cargo run -- server` Then you can launch multiple clients with the commands: - `cargo run -- client -c 1` +- `cargo run -- client -c 2` -- `cargo run -- client -c 2 --client-port 2000` +You can modify the file `assets/settings.ron` to modify some networking settings. -To use webtransport: -- `cargo run -- server --transport web-transport` -- `cargo run -- client -c 1 --transport web-transport` \ No newline at end of file +### Testing in wasm with webtransport + +NOTE: I am using [trunk](https://trunkrs.dev/) to build and serve the wasm example. + +To test the example in wasm, you can run the following commands: `trunk serve` + +You will need a valid SSL certificate to test the example in wasm using webtransport. You will need to run the following +commands: + +- `sh examples/generate.sh` (to generate the temporary SSL certificates, they are only valid for 2 weeks) +- `cargo run -- server` to start the server. The server will print out the certificate digest (something + like `1fd28860bd2010067cee636a64bcbb492142295b297fd8c480e604b70ce4d644`) +- You then have to replace the certificate digest in the `assets/settings.ron` file with the one that the server printed + out. +- then start the client wasm test with `trunk serve` \ No newline at end of file diff --git a/examples/simple_box/assets/settings.ron b/examples/simple_box/assets/settings.ron index 8cfa5c0f..82756d34 100644 --- a/examples/simple_box/assets/settings.ron +++ b/examples/simple_box/assets/settings.ron @@ -1,37 +1,37 @@ Settings( - client: ClientSettings( - inspector: true, - client_id: 0, - client_port: 0, // the OS will assign a random open port - server_addr: "127.0.0.1", - server_port: 5000, - transport: WebTransport( - // this is only needed for wasm, the self-signed certificates are only valid for 2 weeks - // the server will print the certificate digest on startup - certificate_digest: "1fd28860bd2010067cee636a64bcbb492142295b297fd8c480e604b70ce4d644", - ), - // server_port: 5001, - // transport: Udp, - // server_port: 5002, - // transport: WebSocket, - ), - server: ServerSettings( - headless: true, - inspector: false, - transport: [ - WebTransport( - local_port: 5000 - ), - Udp( - local_port: 5001 - ), - WebSocket( - local_port: 5002 - ) - ], - ), - shared: SharedSettings( - protocol_id: 0, - private_key: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), - ) + client: ClientSettings( + inspector: true, + client_id: 0, + client_port: 0, // the OS will assign a random open port + server_addr: "127.0.0.1", + server_port: 5000, + transport: WebTransport( + // this is only needed for wasm, the self-signed certificates are only valid for 2 weeks + // the server will print the certificate digest on startup + certificate_digest: "1fd28860bd2010067cee636a64bcbb492142295b297fd8c480e604b70ce4d644", + ), + // server_port: 5001, + // transport: Udp, + // server_port: 5002, + // transport: WebSocket, + ), + server: ServerSettings( + headless: true, + inspector: false, + transport: [ + WebTransport( + local_port: 5000 + ), + Udp( + local_port: 5001 + ), + WebSocket( + local_port: 5002 + ) + ], + ), + shared: SharedSettings( + protocol_id: 0, + private_key: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + ) ) diff --git a/examples/simple_box/src/client.rs b/examples/simple_box/src/client.rs index 53df6581..2a512664 100644 --- a/examples/simple_box/src/client.rs +++ b/examples/simple_box/src/client.rs @@ -1,16 +1,19 @@ -use crate::protocol::Direction; -use crate::protocol::*; -use crate::shared::{shared_config, shared_movement_behaviour}; -use crate::{shared, ClientTransports, SharedSettings, KEY, PROTOCOL_ID}; +use std::net::{Ipv4Addr, SocketAddr}; +use std::str::FromStr; + use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use bevy::time::common_conditions::on_timer; use bevy::utils::Duration; + use lightyear::client::resource::connect_with_token; use lightyear::prelude::client::*; use lightyear::prelude::*; -use std::net::{Ipv4Addr, SocketAddr}; -use std::str::FromStr; + +use crate::protocol::Direction; +use crate::protocol::*; +use crate::shared::{shared_config, shared_movement_behaviour}; +use crate::{shared, ClientTransports, SharedSettings}; pub struct ClientPluginGroup { client_id: ClientId, @@ -20,25 +23,10 @@ pub struct ClientPluginGroup { impl ClientPluginGroup { pub(crate) fn new( client_id: u64, - client_port: u16, server_addr: SocketAddr, - transport: ClientTransports, + transport_config: TransportConfig, shared_settings: SharedSettings, ) -> ClientPluginGroup { - let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), client_port); - let transport_config = match transport { - #[cfg(not(target_family = "wasm"))] - ClientTransports::Udp => TransportConfig::UdpSocket(client_addr), - ClientTransports::WebTransport { certificate_digest } => { - TransportConfig::WebTransportClient { - client_addr, - server_addr, - #[cfg(target_family = "wasm")] - certificate_digest, - } - } - ClientTransports::WebSocket => TransportConfig::WebSocketClient { server_addr }, - }; let auth = Authentication::Manual { server_addr, client_id, diff --git a/examples/simple_box/src/main.rs b/examples/simple_box/src/main.rs index a98b3c06..f75129e8 100644 --- a/examples/simple_box/src/main.rs +++ b/examples/simple_box/src/main.rs @@ -5,18 +5,11 @@ //! Run with //! - `cargo run -- server` //! - `cargo run -- client -c 1` -mod client; -mod protocol; - -#[cfg(not(target_family = "wasm"))] -mod server; -mod shared; - -use async_compat::Compat; -use bevy::asset::ron; use std::net::{Ipv4Addr, SocketAddr}; use std::str::FromStr; +use async_compat::Compat; +use bevy::asset::ron; use bevy::log::{Level, LogPlugin}; use bevy::prelude::*; use bevy::tasks::IoTaskPool; @@ -25,16 +18,22 @@ use bevy_inspector_egui::quick::WorldInspectorPlugin; use clap::{Parser, ValueEnum}; use serde::{Deserialize, Serialize}; +use lightyear::connection::netcode::ClientId; +use lightyear::prelude::server::Certificate; +use lightyear::prelude::TransportConfig; +use lightyear::shared::log::add_log_layer; +use lightyear::transport::LOCAL_SOCKET; + use crate::client::ClientPluginGroup; #[cfg(not(target_family = "wasm"))] use crate::server::ServerPluginGroup; -use lightyear::connection::netcode::{ClientId, Key}; -use lightyear::prelude::TransportConfig; -use lightyear::shared::log::add_log_layer; -pub const PROTOCOL_ID: u64 = 0; +mod client; +mod protocol; -pub const KEY: Key = [0; 32]; +#[cfg(not(target_family = "wasm"))] +mod server; +mod shared; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum ClientTransports { @@ -53,7 +52,7 @@ pub enum ServerTransports { WebSocket { local_port: u16 }, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct ServerSettings { /// If true, disable any rendering-related plugins headless: bool, @@ -65,7 +64,7 @@ pub struct ServerSettings { transport: Vec, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct ClientSettings { /// If true, enable bevy_inspector_egui inspector: bool, @@ -95,7 +94,7 @@ pub struct SharedSettings { private_key: [u8; 32], } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Settings { pub server: ServerSettings, pub client: ClientSettings, @@ -105,7 +104,15 @@ pub struct Settings { #[derive(Parser, PartialEq, Debug)] enum Cli { #[cfg(not(target_family = "wasm"))] + /// The program will act both as a server and as a client + ListenServer { + #[arg(short, long, default_value = None)] + client_id: Option, + }, + #[cfg(not(target_family = "wasm"))] + /// Dedicated server Server, + /// The program will act as a client Client { #[arg(short, long, default_value = None)] client_id: Option, @@ -125,66 +132,157 @@ fn main() { } let settings_str = include_str!("../assets/settings.ron"); let settings = ron::de::from_str::(settings_str).unwrap(); - let mut app = App::new(); - setup(&mut app, settings, cli); - app.run(); + run(settings, cli); } -fn setup(app: &mut App, settings: Settings, cli: Cli) { +fn run(settings: Settings, cli: Cli) { match cli { + #[cfg(not(target_family = "wasm"))] + Cli::ListenServer { client_id } => { + // create client app + let (from_server_send, from_server_recv) = crossbeam_channel::unbounded(); + let (to_server_send, to_server_recv) = crossbeam_channel::unbounded(); + let transport_config = TransportConfig::LocalChannel { + recv: from_server_recv, + send: to_server_send, + }; + // when communicating via channels, we need to use the address `LOCAL_SOCKET` for the server + let mut client_app = + client_app(settings.clone(), LOCAL_SOCKET, client_id, transport_config); + + // create server app + let extra_transport_configs = vec![TransportConfig::Channels { + // even if we communicate via channels, we need to provide a socket address for the client + channels: vec![(LOCAL_SOCKET, to_server_recv, from_server_send)], + }]; + let mut server_app = server_app(settings, extra_transport_configs); + + // run both the client and server apps + std::thread::spawn(move || server_app.run()); + client_app.run(); + } #[cfg(not(target_family = "wasm"))] Cli::Server => { - let shared = settings.shared; - let settings = settings.server; - if !settings.headless { - app.add_plugins(DefaultPlugins.build().disable::()); - } else { - app.add_plugins(MinimalPlugins); - } - app.add_plugins(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), - update_subscriber: Some(add_log_layer), - }); - - if settings.inspector { - app.add_plugins(WorldInspectorPlugin::new()); - } - // this is async because we need to load the certificate from io - // we need async_compat because wtransport expects a tokio reactor - let server_plugin_group = IoTaskPool::get() - .scope(|s| { - s.spawn(Compat::new(async { - ServerPluginGroup::new(settings.transport, shared).await - })); - }) - .pop() - .unwrap(); - app.add_plugins(server_plugin_group.build()); + let mut app = server_app(settings, vec![]); + app.run(); } Cli::Client { client_id } => { - let shared = settings.shared; - let settings = settings.client; - // NOTE: create the default plugins first so that the async task pools are initialized - // use the default bevy logger for now - // (the lightyear logger doesn't handle wasm) - app.add_plugins(DefaultPlugins.build().set(LogPlugin { - level: Level::INFO, - filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), - update_subscriber: Some(add_log_layer), - })); - if settings.inspector { - app.add_plugins(WorldInspectorPlugin::new()); + let server_addr = SocketAddr::new( + settings.client.server_addr.into(), + settings.client.server_port, + ); + let transport_config = get_client_transport_config(settings.client.clone()); + let mut app = client_app(settings, server_addr, client_id, transport_config); + app.run(); + } + } +} + +/// Build the client app +fn client_app( + settings: Settings, + server_addr: SocketAddr, + client_id: Option, + transport_config: TransportConfig, +) -> App { + let mut app = App::new(); + // NOTE: create the default plugins first so that the async task pools are initialized + // use the default bevy logger for now + // (the lightyear logger doesn't handle wasm) + app.add_plugins(DefaultPlugins.build().set(LogPlugin { + level: Level::INFO, + filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), + update_subscriber: Some(add_log_layer), + })); + if settings.client.inspector { + app.add_plugins(WorldInspectorPlugin::new()); + } + let client_plugin_group = ClientPluginGroup::new( + // use the cli-provided client id if it exists, otherwise use the settings client id + client_id.unwrap_or(settings.client.client_id), + server_addr, + transport_config, + settings.shared, + ); + app.add_plugins(client_plugin_group.build()); + app +} + +/// Build the server app +fn server_app(settings: Settings, extra_transport_configs: Vec) -> App { + let mut app = App::new(); + if !settings.server.headless { + app.add_plugins(DefaultPlugins.build().disable::()); + } else { + app.add_plugins(MinimalPlugins); + } + app.add_plugins(LogPlugin { + level: Level::INFO, + filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), + update_subscriber: Some(add_log_layer), + }); + + if settings.server.inspector { + app.add_plugins(WorldInspectorPlugin::new()); + } + let mut transport_configs = get_server_transport_configs(settings.server.transport); + transport_configs.extend(extra_transport_configs); + let server_plugin_group = ServerPluginGroup::new(transport_configs, settings.shared); + app.add_plugins(server_plugin_group.build()); + app +} + +/// Parse the server transport settings into a list of `TransportConfig` that are used to configure the lightyear server +fn get_server_transport_configs(settings: Vec) -> Vec { + settings + .iter() + .map(|t| match t { + ServerTransports::Udp { local_port } => TransportConfig::UdpSocket(SocketAddr::new( + Ipv4Addr::UNSPECIFIED.into(), + *local_port, + )), + ServerTransports::WebTransport { local_port } => { + // this is async because we need to load the certificate from io + // we need async_compat because wtransport expects a tokio reactor + let certificate = IoTaskPool::get() + .scope(|s| { + s.spawn(Compat::new(async { + Certificate::load("../certificates/cert.pem", "../certificates/key.pem") + .await + .unwrap() + })); + }) + .pop() + .unwrap(); + let digest = &certificate.hashes()[0].to_string().replace(":", ""); + println!("Generated self-signed certificate with digest: {}", digest); + TransportConfig::WebTransportServer { + server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), + certificate, + } } - let server_addr = SocketAddr::new(settings.server_addr.into(), settings.server_port); - let client_plugin_group = ClientPluginGroup::new( - client_id.unwrap_or(settings.client_id), - settings.client_port, + ServerTransports::WebSocket { local_port } => TransportConfig::WebSocketServer { + server_addr: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port), + }, + }) + .collect() +} + +/// Parse the client transport settings into a `TransportConfig` that is used to configure the lightyear client +fn get_client_transport_config(settings: ClientSettings) -> TransportConfig { + let server_addr = SocketAddr::new(settings.server_addr.into(), settings.server_port); + let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), settings.client_port); + match settings.transport { + #[cfg(not(target_family = "wasm"))] + ClientTransports::Udp => TransportConfig::UdpSocket(client_addr), + ClientTransports::WebTransport { certificate_digest } => { + TransportConfig::WebTransportClient { + client_addr, server_addr, - settings.transport, - shared, - ); - app.add_plugins(client_plugin_group.build()); + #[cfg(target_family = "wasm")] + certificate_digest, + } } + ClientTransports::WebSocket => TransportConfig::WebSocketClient { server_addr }, } } diff --git a/examples/simple_box/src/protocol.rs b/examples/simple_box/src/protocol.rs index 51a8b6aa..068addf6 100644 --- a/examples/simple_box/src/protocol.rs +++ b/examples/simple_box/src/protocol.rs @@ -1,11 +1,13 @@ +use std::ops::Mul; + use bevy::ecs::entity::MapEntities; use bevy::prelude::{ default, Bundle, Color, Component, Deref, DerefMut, Entity, EntityMapper, Vec2, }; use derive_more::{Add, Mul}; -use lightyear::prelude::*; use serde::{Deserialize, Serialize}; -use std::ops::Mul; + +use lightyear::prelude::*; // Player #[derive(Bundle)] diff --git a/examples/simple_box/src/server.rs b/examples/simple_box/src/server.rs index 1b679b9e..1ab80154 100644 --- a/examples/simple_box/src/server.rs +++ b/examples/simple_box/src/server.rs @@ -1,13 +1,16 @@ -use crate::protocol::*; -use crate::shared::{shared_config, shared_movement_behaviour}; -use crate::{shared, ServerTransports, SharedSettings, KEY, PROTOCOL_ID}; +use std::collections::HashMap; +use std::net::{Ipv4Addr, SocketAddr}; + use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use bevy::utils::Duration; + use lightyear::prelude::server::*; use lightyear::prelude::*; -use std::collections::HashMap; -use std::net::{Ipv4Addr, SocketAddr}; + +use crate::protocol::*; +use crate::shared::{shared_config, shared_movement_behaviour}; +use crate::{shared, ServerTransports, SharedSettings}; // Plugin group to add all server-related plugins pub struct ServerPluginGroup { @@ -15,8 +18,8 @@ pub struct ServerPluginGroup { } impl ServerPluginGroup { - pub(crate) async fn new( - transports: Vec, + pub(crate) fn new( + transport_configs: Vec, shared_settings: SharedSettings, ) -> ServerPluginGroup { // Step 1: create the io (transport + link conditioner) @@ -26,31 +29,7 @@ impl ServerPluginGroup { incoming_loss: 0.05, }; let mut net_configs = vec![]; - for transport in &transports { - let transport_config = match transport { - ServerTransports::Udp { local_port } => { - let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port); - TransportConfig::UdpSocket(server_addr) - } - // if using webtransport, we load the certificate keys - ServerTransports::WebTransport { local_port } => { - let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port); - let certificate = - Certificate::load("../certificates/cert.pem", "../certificates/key.pem") - .await - .unwrap(); - let digest = &certificate.hashes()[0].to_string().replace(":", ""); - println!("Generated self-signed certificate with digest: {}", digest); - TransportConfig::WebTransportServer { - server_addr, - certificate, - } - } - ServerTransports::WebSocket { local_port } => { - let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *local_port); - TransportConfig::WebSocketServer { server_addr } - } - }; + for transport_config in transport_configs { net_configs.push(NetConfig::Netcode { config: NetcodeConfig::default() .with_protocol_id(shared_settings.protocol_id) @@ -62,7 +41,7 @@ impl ServerPluginGroup { // Step 2: define the server configuration let config = ServerConfig { - shared: shared_config().clone(), + shared: shared_config(), net: net_configs, ..default() }; diff --git a/examples/simple_box/src/shared.rs b/examples/simple_box/src/shared.rs index a9afe3e1..637575ca 100644 --- a/examples/simple_box/src/shared.rs +++ b/examples/simple_box/src/shared.rs @@ -1,10 +1,10 @@ -use crate::protocol::*; -use bevy::diagnostic::LogDiagnosticsPlugin; use bevy::prelude::*; use bevy::render::RenderPlugin; use bevy::utils::Duration; + use lightyear::prelude::*; -use lightyear::transport::io::IoDiagnosticsPlugin; + +use crate::protocol::*; pub fn shared_config() -> SharedConfig { SharedConfig { diff --git a/examples/stepper/src/bin/headless.rs b/examples/stepper/src/bin/headless.rs index 845289d4..20b892f3 100644 --- a/examples/stepper/src/bin/headless.rs +++ b/examples/stepper/src/bin/headless.rs @@ -1,4 +1,5 @@ #![cfg(not(target_family = "wasm"))] + use std::net::SocketAddr; use std::str::FromStr; diff --git a/examples/stepper/src/bin/input_buffer.rs b/examples/stepper/src/bin/input_buffer.rs index d2757f0b..2f42f32f 100644 --- a/examples/stepper/src/bin/input_buffer.rs +++ b/examples/stepper/src/bin/input_buffer.rs @@ -2,7 +2,6 @@ #![allow(unused_variables)] #![allow(dead_code)] -use bevy::utils::{Duration, Instant}; use std::net::SocketAddr; use std::str::FromStr; @@ -12,6 +11,7 @@ use bevy::prelude::{ Startup, Time, }; use bevy::time::TimeUpdateStrategy; +use bevy::utils::{Duration, Instant}; use bevy::winit::WinitPlugin; use bevy::{DefaultPlugins, MinimalPlugins}; use tracing::{debug, info}; diff --git a/examples/stepper/src/bin/prediction.rs b/examples/stepper/src/bin/prediction.rs index 7ad4f775..9d99c53c 100644 --- a/examples/stepper/src/bin/prediction.rs +++ b/examples/stepper/src/bin/prediction.rs @@ -2,7 +2,6 @@ #![allow(unused_variables)] #![allow(dead_code)] -use bevy::utils::{Duration, Instant}; use std::net::SocketAddr; use std::str::FromStr; @@ -12,6 +11,7 @@ use bevy::prelude::{ ResMut, Startup, Time, With, }; use bevy::time::TimeUpdateStrategy; +use bevy::utils::{Duration, Instant}; use bevy::winit::WinitPlugin; use bevy::{DefaultPlugins, MinimalPlugins}; use tracing::{debug, info}; diff --git a/examples/stepper/src/bin/simple.rs b/examples/stepper/src/bin/simple.rs index 27e8d2d0..8dd63c66 100644 --- a/examples/stepper/src/bin/simple.rs +++ b/examples/stepper/src/bin/simple.rs @@ -1,4 +1,5 @@ #![cfg(not(target_family = "wasm"))] + use std::net::SocketAddr; use std::str::FromStr; diff --git a/examples/stepper/src/bin/step.rs b/examples/stepper/src/bin/step.rs index 0fc9b6b4..7ec7fe28 100644 --- a/examples/stepper/src/bin/step.rs +++ b/examples/stepper/src/bin/step.rs @@ -3,7 +3,6 @@ #![allow(unused_variables)] #![allow(dead_code)] -use bevy::utils::{Duration, Instant}; use std::net::SocketAddr; use std::str::FromStr; @@ -11,6 +10,7 @@ use bevy::log::LogPlugin; use bevy::prelude::default; use bevy::prelude::{App, Commands, PluginGroup, Real, ResMut, Startup, Time}; use bevy::time::TimeUpdateStrategy; +use bevy::utils::{Duration, Instant}; use bevy::winit::WinitPlugin; use bevy::{DefaultPlugins, MinimalPlugins}; use tracing::{debug, info}; diff --git a/examples/stepper/src/client.rs b/examples/stepper/src/client.rs index 06b5a247..4b0fa79a 100644 --- a/examples/stepper/src/client.rs +++ b/examples/stepper/src/client.rs @@ -1,9 +1,9 @@ -use bevy::utils::Duration; use std::net::SocketAddr; use std::str::FromStr; use bevy::app::App; use bevy::prelude::default; +use bevy::utils::Duration; use lightyear::prelude::client::*; use lightyear::prelude::*; diff --git a/examples/stepper/src/server.rs b/examples/stepper/src/server.rs index 9d301870..a75611df 100644 --- a/examples/stepper/src/server.rs +++ b/examples/stepper/src/server.rs @@ -1,15 +1,15 @@ -use bevy::utils::Duration; use std::default::Default; use std::net::SocketAddr; -use std::str::FromStr; use bevy::app::App; use bevy::prelude::default; +use bevy::utils::Duration; -use crate::protocol::*; use lightyear::prelude::server::*; use lightyear::prelude::*; +use crate::protocol::*; + pub fn bevy_setup(app: &mut App, addr: SocketAddr, protocol_id: u64, private_key: Key) { // create udp-socket based io let netcode_config = NetcodeConfig::default() diff --git a/examples/stepper/src/stepper.rs b/examples/stepper/src/stepper.rs index 067ddad8..e87d331d 100644 --- a/examples/stepper/src/stepper.rs +++ b/examples/stepper/src/stepper.rs @@ -1,20 +1,17 @@ -use bevy::utils::Duration; use std::net::SocketAddr; use std::str::FromStr; use bevy::prelude::{default, App, Mut, PluginGroup, Real, Time}; use bevy::time::TimeUpdateStrategy; +use bevy::utils::Duration; use bevy::MinimalPlugins; -use tracing_subscriber::fmt::format::FmtSpan; -use lightyear::client as lightyear_client; use lightyear::connection::netcode::generate_key; use lightyear::prelude::client::{ - Authentication, ClientConfig, InputConfig, InterpolationConfig, PredictionConfig, SyncConfig, + Authentication, ClientConfig, InterpolationConfig, PredictionConfig, SyncConfig, }; use lightyear::prelude::server::{NetcodeConfig, ServerConfig}; use lightyear::prelude::*; -use lightyear::server as lightyear_server; use crate::protocol::*; diff --git a/lightyear/src/connection/netcode/client.rs b/lightyear/src/connection/netcode/client.rs index ace6f8be..e54e9722 100644 --- a/lightyear/src/connection/netcode/client.rs +++ b/lightyear/src/connection/netcode/client.rs @@ -371,6 +371,7 @@ impl NetcodeClient { } fn process_packet(&mut self, addr: SocketAddr, packet: Packet) -> Result<()> { if addr != self.server_addr() { + debug!(?addr, server_addr = ?self.server_addr(), "wrong addr"); return Ok(()); } match (packet, self.state) {