diff --git a/examples/common/src/settings.rs b/examples/common/src/settings.rs index 89beb5916..bf6249139 100644 --- a/examples/common/src/settings.rs +++ b/examples/common/src/settings.rs @@ -272,7 +272,7 @@ pub(crate) fn build_client_netcode_config( /// Parse the settings into a `NetConfig` that is used to configure how the lightyear client /// connects to the server -pub(crate) fn get_client_net_config(settings: &Settings, client_id: u64) -> client::NetConfig { +pub fn get_client_net_config(settings: &Settings, client_id: u64) -> client::NetConfig { let server_addr = SocketAddr::new( settings.client.server_addr.into(), settings.client.server_port, diff --git a/examples/lobby/src/client.rs b/examples/lobby/src/client.rs index ed45ad1fa..9ef9d0037 100644 --- a/examples/lobby/src/client.rs +++ b/examples/lobby/src/client.rs @@ -39,6 +39,7 @@ impl Plugin for ExampleClientPlugin { app.init_resource::(); app.init_resource::(); app.init_state::(); + app.add_systems(Startup, on_disconnect); app.add_systems(PreUpdate, handle_connection.after(MainSet::Receive)); app.add_systems( FixedPreUpdate, @@ -371,7 +372,7 @@ mod lobby { AppState::Game => {} }; match state.get() { - NetworkingState::Disconnected => { + NetworkingState::Disconnected | NetworkingState::None => { if ui.button("Join lobby list").clicked() { // TODO: before connecting, we want to adjust all clients ConnectionConfig to respect the new host // - the new host must run in host-server diff --git a/examples/simple_box/src/client.rs b/examples/simple_box/src/client.rs index 72ba3191e..bb0f0cedd 100644 --- a/examples/simple_box/src/client.rs +++ b/examples/simple_box/src/client.rs @@ -26,7 +26,10 @@ pub struct ExampleClientPlugin; impl Plugin for ExampleClientPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, spawn_connect_button); - app.add_systems(PreUpdate, handle_connection.after(MainSet::Receive)); + app.add_systems( + PreUpdate, + (handle_connection, handle_disconnection).after(MainSet::Receive), + ); // Inputs have to be buffered in the FixedPreUpdate schedule app.add_systems( FixedPreUpdate, @@ -75,6 +78,15 @@ pub(crate) fn handle_connection( } } +/// Listen for events to know when the client is disconnected, and print out the reason +/// of the disconnection +pub(crate) fn handle_disconnection(mut events: EventReader) { + for event in events.read() { + let reason = &event.reason; + error!("Disconnected from server: {:?}", reason); + } +} + /// System that reads from peripherals and adds inputs to the buffer /// This system must be run in the `InputSystemSet::BufferInputs` set in the `FixedPreUpdate` schedule /// to work correctly. @@ -262,7 +274,7 @@ fn button_system( for (entity, children, mut on_click) in &mut interaction_query { let mut text = text_query.get_mut(children[0]).unwrap(); match state.get() { - NetworkingState::Disconnected => { + NetworkingState::Disconnected | NetworkingState::None => { text.sections[0].value = "Connect".to_string(); *on_click = On::>::run(|mut commands: Commands| { commands.connect_client(); diff --git a/lightyear/src/client/events.rs b/lightyear/src/client/events.rs index 27b635cc6..d11316ac4 100644 --- a/lightyear/src/client/events.rs +++ b/lightyear/src/client/events.rs @@ -17,6 +17,7 @@ use bevy::app::{App, Plugin, PreUpdate}; use bevy::prelude::{Component, Event, Events, IntoSystemConfigs}; use crate::client::connection::ConnectionManager; +use crate::connection::client::DisconnectReason; use crate::prelude::ClientId; use crate::shared::events::plugin::EventsPlugin; use crate::shared::events::systems::push_component_events; @@ -66,7 +67,9 @@ impl ConnectEvent { /// Bevy [`Event`] emitted on the client on the frame where the connection is disconnected #[derive(Event, Default)] -pub struct DisconnectEvent; +pub struct DisconnectEvent { + pub reason: Option, +} /// Bevy [`Event`] emitted on the client to indicate the user input for the tick pub type InputEvent = crate::shared::events::components::InputEvent; diff --git a/lightyear/src/client/networking.rs b/lightyear/src/client/networking.rs index 76765b1e6..5372ef4a3 100644 --- a/lightyear/src/client/networking.rs +++ b/lightyear/src/client/networking.rs @@ -17,7 +17,9 @@ use crate::client::interpolation::Interpolated; use crate::client::io::ClientIoEvent; use crate::client::prediction::Predicted; use crate::client::sync::SyncSet; -use crate::connection::client::{ClientConnection, NetClient, NetConfig}; +use crate::connection::client::{ + ClientConnection, ConnectionState, DisconnectReason, NetClient, NetConfig, +}; use crate::connection::server::{IoConfig, ServerConnections}; use crate::prelude::{ is_host_server, ChannelRegistry, MainSet, MessageRegistry, SharedConfig, TickManager, @@ -127,14 +129,6 @@ impl Plugin for ClientNetworkingPlugin { pub(crate) fn receive(world: &mut World) { trace!("Receive server packets"); - // TODO: here we can control time elapsed from the client's perspective? - - // TODO: THE CLIENT COULD DO PHYSICS UPDATES INSIDE FIXED-UPDATE SYSTEMS - // WE SHOULD BE CALLING UPDATE INSIDE THOSE AS WELL SO THAT WE CAN SEND UPDATES - // IN THE MIDDLE OF THE FIXED UPDATE LOOPS - // WE JUST KEEP AN INTERNAL TIMER TO KNOW IF WE REACHED OUR TICK AND SHOULD RECEIVE/SEND OUT PACKETS? - // FIXED-UPDATE.expend() updates the clock zR the fixed update interval - // THE NETWORK TICK INTERVAL COULD BE IN BETWEEN FIXED UPDATE INTERVALS world.resource_scope( |world: &mut World, mut connection: Mut| { world.resource_scope( @@ -152,7 +146,7 @@ pub(crate) fn receive(world: &mut World) { time_manager.update(delta); trace!(time = ?time_manager.current_time(), tick = ?tick_manager.tick(), "receive"); - if netclient.state() != NetworkingState::Disconnected { + if !matches!(netclient.state(), ConnectionState::Disconnected {..}){ let _ = netclient .try_update(delta.as_secs_f64()) .map_err(|e| { @@ -160,7 +154,7 @@ pub(crate) fn receive(world: &mut World) { }); } - if netclient.state() == NetworkingState::Connected { + if matches!(netclient.state(), ConnectionState::Connected) { // we just connected, do a state transition if state.get() != &NetworkingState::Connected { debug!("Setting the networking state to connected"); @@ -174,7 +168,8 @@ pub(crate) fn receive(world: &mut World) { tick_manager.as_ref(), ); } - if netclient.state() == NetworkingState::Disconnected { + if let ConnectionState::Disconnected{reason} = netclient.state() { + netclient.disconnect_reason = reason; // we just disconnected, do a state transition if state.get() != &NetworkingState::Disconnected { next_state.set(NetworkingState::Disconnected); @@ -273,8 +268,10 @@ pub(crate) fn sync_update( /// Bevy [`State`] representing the networking state of the client. #[derive(States, Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum NetworkingState { - /// The client is disconnected from the server. The receive/send packets systems do not run. + /// Starting state (to avoid running the OnDisconnect schedule when starting the app) #[default] + None, + /// The client is disconnected from the server. The receive/send packets systems do not run. Disconnected, /// The client is trying to connect to the server Connecting, @@ -298,6 +295,7 @@ fn listen_io_state( Ok(ClientIoEvent::Disconnected(e)) => { error!("Error from io: {}", e); io.state = IoState::Disconnected; + netclient.disconnect_reason = Some(DisconnectReason::Transport(e)); disconnect = true; } Err(TryRecvError::Empty) => { @@ -305,6 +303,9 @@ fn listen_io_state( } Err(TryRecvError::Closed) => { error!("Io status channel has been closed when it shouldn't be"); + netclient.disconnect_reason = Some(DisconnectReason::Transport( + std::io::Error::other("Io status channel has been closed").into(), + )); disconnect = true; } } @@ -313,6 +314,7 @@ fn listen_io_state( if disconnect { debug!("Going to NetworkingState::Disconnected because of io error."); next_state.set(NetworkingState::Disconnected); + // TODO: do we need to disconnect here? we disconnect in the OnEnter(Disconnected) system anyway let _ = netclient .disconnect() .inspect_err(|e| debug!("error disconnecting netclient: {e:?}")); @@ -357,7 +359,7 @@ fn on_connect_host_server( fn on_disconnect( mut connection_manager: ResMut, mut disconnect_event_writer: EventWriter, - mut netcode: ResMut, + mut netclient: ResMut, mut commands: Commands, received_entities: Query, With, With)>>, ) { @@ -371,11 +373,12 @@ fn on_disconnect( connection_manager.sync_manager.synced = false; // try to disconnect again to close io tasks (in case the disconnection is from the io) - let _ = netcode.disconnect(); + let _ = netclient.disconnect(); // no need to update the io state, because we will recreate a new `ClientConnection` // for the next connection attempt - disconnect_event_writer.send(DisconnectEvent); + let reason = std::mem::take(&mut netclient.disconnect_reason); + disconnect_event_writer.send(DisconnectEvent { reason }); // TODO: remove ClientConnection and ConnectionManager resources? } @@ -455,8 +458,10 @@ fn connect(world: &mut World) { }); let config = world.resource::(); - if world.resource::().state() == NetworkingState::Connected - && config.shared.mode == Mode::HostServer + if matches!( + world.resource::().state(), + ConnectionState::Connected + ) && config.shared.mode == Mode::HostServer { // TODO: also check if the connection is of type local? // in host server mode, there is no connecting phase, we directly become connected diff --git a/lightyear/src/connection/client.rs b/lightyear/src/connection/client.rs index 678b43065..5c51cf5d6 100644 --- a/lightyear/src/connection/client.rs +++ b/lightyear/src/connection/client.rs @@ -21,6 +21,13 @@ use crate::prelude::client::ClientTransport; use crate::prelude::{generate_key, Key, LinkConditionerConfig}; use crate::transport::config::SharedIoConfig; +#[derive(Debug)] +pub enum ConnectionState { + Disconnected { reason: Option }, + Connecting, + Connected, +} + // TODO: add diagnostics methods? #[enum_dispatch] pub trait NetClient: Send + Sync { @@ -32,8 +39,8 @@ pub trait NetClient: Send + Sync { /// Disconnect from the server fn disconnect(&mut self) -> Result<()>; - /// Returns the [`NetworkingState`] of the client - fn state(&self) -> NetworkingState; + /// Returns the [`ConnectionState`] of the client + fn state(&self) -> ConnectionState; /// Update the connection state + internal bookkeeping (keep-alives, etc.) fn try_update(&mut self, delta_ms: f64) -> Result<()>; @@ -70,6 +77,16 @@ pub enum NetClientDispatch { #[derive(Resource)] pub struct ClientConnection { pub client: NetClientDispatch, + pub(crate) disconnect_reason: Option, +} + +/// Enumerates the possible reasons for a client to disconnect from the server +#[derive(Debug)] +pub enum DisconnectReason { + Transport(crate::transport::error::Error), + Netcode(super::netcode::ClientState), + #[cfg(all(feature = "steam", not(target_family = "wasm")))] + Steam(steamworks::networking_types::NetConnectionEnd), } pub type IoConfig = SharedIoConfig; @@ -131,6 +148,7 @@ impl NetConfig { }; ClientConnection { client: NetClientDispatch::Netcode(client), + disconnect_reason: None, } } #[cfg(all(feature = "steam", not(target_family = "wasm")))] @@ -150,12 +168,14 @@ impl NetConfig { .expect("could not create steam client"); ClientConnection { client: NetClientDispatch::Steam(client), + disconnect_reason: None, } } NetConfig::Local { id } => { let client = super::local::client::Client::new(id); ClientConnection { client: NetClientDispatch::Local(client), + disconnect_reason: None, } } } @@ -171,7 +191,7 @@ impl NetClient for ClientConnection { self.client.disconnect() } - fn state(&self) -> NetworkingState { + fn state(&self) -> ConnectionState { self.client.state() } diff --git a/lightyear/src/connection/local/client.rs b/lightyear/src/connection/local/client.rs index a1812b7b7..265f1a4ca 100644 --- a/lightyear/src/connection/local/client.rs +++ b/lightyear/src/connection/local/client.rs @@ -1,6 +1,6 @@ use crate::client::io::Io; use crate::client::networking::NetworkingState; -use crate::connection::client::NetClient; +use crate::connection::client::{ConnectionState, NetClient}; use crate::packet::packet::Packet; use crate::prelude::ClientId; use crate::transport::LOCAL_SOCKET; @@ -33,11 +33,11 @@ impl NetClient for Client { Ok(()) } - fn state(&self) -> NetworkingState { + fn state(&self) -> ConnectionState { if self.is_connected { - NetworkingState::Connected + ConnectionState::Connected } else { - NetworkingState::Disconnected + ConnectionState::Disconnected { reason: None } } } diff --git a/lightyear/src/connection/netcode/client.rs b/lightyear/src/connection/netcode/client.rs index 946009972..4357f2bcf 100644 --- a/lightyear/src/connection/netcode/client.rs +++ b/lightyear/src/connection/netcode/client.rs @@ -7,7 +7,7 @@ use bevy::prelude::Resource; use tracing::{debug, error, info, trace, warn}; use crate::client::io::Io; -use crate::connection::client::{IoConfig, NetClient}; +use crate::connection::client::{ConnectionState, DisconnectReason, IoConfig, NetClient}; use crate::connection::id; use crate::prelude::client::NetworkingState; use crate::serialize::bitcode::reader::BufferPool; @@ -679,13 +679,15 @@ impl NetClient for Client { Ok(()) } - fn state(&self) -> NetworkingState { + fn state(&self) -> ConnectionState { match self.client.state() { ClientState::SendingConnectionRequest | ClientState::SendingChallengeResponse => { - NetworkingState::Connecting + ConnectionState::Connecting } - ClientState::Connected => NetworkingState::Connected, - _ => NetworkingState::Disconnected, + ClientState::Connected => ConnectionState::Connected, + _ => ConnectionState::Disconnected { + reason: Some(DisconnectReason::Netcode(self.client.state)), + }, } } diff --git a/lightyear/src/connection/steam/client.rs b/lightyear/src/connection/steam/client.rs index a73f83851..156dee800 100644 --- a/lightyear/src/connection/steam/client.rs +++ b/lightyear/src/connection/steam/client.rs @@ -1,5 +1,5 @@ use crate::client::networking::NetworkingState; -use crate::connection::client::NetClient; +use crate::connection::client::{ConnectionState, DisconnectReason, NetClient}; use crate::connection::id::ClientId; use crate::packet::packet::Packet; use crate::prelude::client::Io; @@ -153,16 +153,23 @@ impl NetClient for Client { Ok(()) } - fn state(&self) -> NetworkingState { + fn state(&self) -> ConnectionState { match self .connection_state() .unwrap_or(NetworkingConnectionState::None) { NetworkingConnectionState::Connecting | NetworkingConnectionState::FindingRoute => { - NetworkingState::Connecting + ConnectionState::Connecting + } + NetworkingConnectionState::Connected => ConnectionState::Connected, + _ => { + let reason = self + .connection_info() + .map_or(None, |info| info.ok().map(|i| i.end_reason())) + .flatten() + .map(|r| DisconnectReason::Steam(r)); + ConnectionState::Disconnected { reason } } - NetworkingConnectionState::Connected => NetworkingState::Connected, - _ => NetworkingState::Disconnected, } } diff --git a/lightyear/src/shared/run_conditions.rs b/lightyear/src/shared/run_conditions.rs index cc899fa66..ce556ea2a 100644 --- a/lightyear/src/shared/run_conditions.rs +++ b/lightyear/src/shared/run_conditions.rs @@ -1,7 +1,7 @@ //! Common run conditions use crate::client::networking::NetworkingState; -use crate::connection::client::{ClientConnection, NetClient}; +use crate::connection::client::{ClientConnection, ConnectionState, NetClient}; use crate::connection::server::ServerConnections; use crate::prelude::server::ServerConfig; use crate::prelude::{Mode, NetworkIdentity}; @@ -46,7 +46,7 @@ pub fn is_mode_separate(config: Option>) -> bool { /// to avoid having a frame of delay since the `StateTransition` schedule runs after `PreUpdate`. /// We also check both the networking state and the io state (in case the io gets disconnected) pub fn is_connected(netclient: Option>) -> bool { - netclient.map_or(false, |c| c.state() == NetworkingState::Connected) + netclient.map_or(false, |c| matches!(c.state(), ConnectionState::Connected)) } /// Returns true if the client is disconnected. @@ -54,9 +54,9 @@ pub fn is_connected(netclient: Option>) -> bool { /// We check the status of the ClientConnection directly instead of using the `State` /// to avoid having a frame of delay since the `StateTransition` schedule runs after `PreUpdate` pub fn is_disconnected(netclient: Option>) -> bool { - netclient - .as_ref() - .map_or(true, |c| c.state() == NetworkingState::Disconnected) + netclient.as_ref().map_or(true, |c| { + matches!(c.state(), ConnectionState::Disconnected { .. }) + }) } /// Returns true if the server is started.