diff --git a/kubi/src/networking.rs b/kubi/src/networking.rs index 9e1fe28..9dcdd88 100644 --- a/kubi/src/networking.rs +++ b/kubi/src/networking.rs @@ -1,198 +1,201 @@ -use shipyard::{Unique, AllStoragesView, UniqueView, UniqueViewMut, Workload, IntoWorkload, EntitiesViewMut, Component, ViewMut, SystemModificator, View, IntoIter, WorkloadModificator}; -use winit::event_loop::ControlFlow; -use std::net::SocketAddr; -use uflow::{ - client::{Client, Config as ClientConfig, Event as ClientEvent}, - EndpointConfig -}; -use kubi_shared::networking::{ - messages::ServerToClientMessage, - state::ClientJoinState, - client::ClientIdMap, -}; -use crate::{ - events::EventComponent, - control_flow::RequestExit, - world::tasks::ChunkTaskManager, - state::is_ingame_or_loading, - fixed_timestamp::FixedTimestamp -}; - -mod handshake; -mod world; -mod player; - -use handshake::{ - set_client_join_state_to_connected, - say_hello, - check_server_hello_response -}; -use world::{ - inject_network_responses_into_manager_queue, - send_block_place_events, - recv_block_place_events, -}; -use player::{ - init_client_map, - send_player_movement_events, - receive_player_movement_events, - receive_player_connect_events -}; - -const NET_TICKRATE: u16 = 33; - -#[derive(Unique, Clone, Copy, PartialEq, Eq)] -pub enum GameType { - Singleplayer, - Muliplayer -} - -#[derive(Unique, Clone, Copy, PartialEq, Eq)] -pub struct ServerAddress(pub SocketAddr); - -#[derive(Unique)] -pub struct UdpClient(pub Client); - -#[derive(Component)] -pub struct NetworkEvent(pub ClientEvent); - -impl NetworkEvent { - ///Checks if postcard-encoded message has a type - pub fn is_message_of_type(&self) -> bool { - let ClientEvent::Receive(data) = &self.0 else { return false }; - if data.len() == 0 { return false } - data[0] == T - } -} - -#[derive(Component)] -pub struct NetworkMessageEvent(pub ServerToClientMessage); - -fn connect_client( - storages: AllStoragesView -) { - log::info!("Creating client"); - let address = storages.borrow::>().unwrap(); - let client = Client::connect(address.0, ClientConfig { - endpoint_config: EndpointConfig { - active_timeout_ms: 10000, - keepalive: true, - keepalive_interval_ms: 5000, - ..Default::default() - }, - }).expect("Client connection failed"); - storages.add_unique(UdpClient(client)); - storages.add_unique(ClientJoinState::Disconnected); -} - -fn poll_client( - mut client: UniqueViewMut, - mut entities: EntitiesViewMut, - mut events: ViewMut, - mut network_events: ViewMut, -) { - entities.bulk_add_entity(( - &mut events, - &mut network_events, - ), client.0.step().map(|event| { - (EventComponent, NetworkEvent(event)) - })); -} - -fn flush_client( - mut client: UniqueViewMut, -) { - client.0.flush(); -} - -fn handle_disconnect( - network_events: View, - mut join_state: UniqueViewMut -) { - for event in network_events.iter() { - if matches!(event.0, ClientEvent::Disconnect) { - log::warn!("Disconnected from server"); - *join_state = ClientJoinState::Disconnected; - return; - } - } -} - -pub fn update_networking() -> Workload { - ( - init_client_map.run_if_missing_unique::(), - connect_client.run_if_missing_unique::(), - poll_client.into_workload().make_fixed(NET_TICKRATE, 0), - ( - set_client_join_state_to_connected, - say_hello, - ).into_sequential_workload().run_if(if_just_connected), - ( - check_server_hello_response, - handle_disconnect, - ).into_sequential_workload().run_if(is_join_state::<{ClientJoinState::Connected as u8}>), - ( - ( - receive_player_connect_events, - ).into_workload(), - ( - recv_block_place_events, - receive_player_movement_events, - ).into_workload() - ).into_sequential_workload().run_if(is_join_state::<{ClientJoinState::Joined as u8}>).run_if(is_ingame_or_loading), - inject_network_responses_into_manager_queue.run_if(is_ingame_or_loading).skip_if_missing_unique::(), - ).into_sequential_workload() -} - -pub fn update_networking_late() -> Workload { - ( - ( - send_block_place_events, - send_player_movement_events, - ).into_workload().run_if(is_join_state::<{ClientJoinState::Joined as u8}>), - flush_client.into_workload().make_fixed(NET_TICKRATE, 1) - ).into_sequential_workload() -} - -pub fn disconnect_on_exit( - exit: UniqueView, - mut client: UniqueViewMut, -) { - //TODO check if this works - if exit.0 { - if client.0.is_active() { - client.0.flush(); - client.0.disconnect(); - while client.0.is_active() { client.0.step().for_each(|_|()); } - log::info!("Client disconnected"); - } else { - log::info!("Client inactive") - } - } -} - -// conditions - -fn if_just_connected( - network_events: View, -) -> bool { - network_events.iter().any(|event| matches!(&event.0, ClientEvent::Connect)) -} - -fn is_join_state( - join_state: UniqueView -) -> bool { - (*join_state as u8) == STATE -} - -pub fn is_multiplayer( - game_type: UniqueView -) -> bool { - *game_type == GameType::Muliplayer -} - -pub fn is_singleplayer( - game_type: UniqueView -) -> bool { - *game_type == GameType::Singleplayer -} +use shipyard::{Unique, AllStoragesView, UniqueView, UniqueViewMut, Workload, IntoWorkload, EntitiesViewMut, Component, ViewMut, SystemModificator, View, IntoIter, WorkloadModificator}; +use winit::event_loop::ControlFlow; +use std::net::SocketAddr; +use uflow::{ + client::{Client, Config as ClientConfig, Event as ClientEvent}, + EndpointConfig +}; +use kubi_shared::networking::{ + messages::ServerToClientMessage, + state::ClientJoinState, + client::ClientIdMap, +}; +use crate::{ + events::EventComponent, + control_flow::RequestExit, + world::tasks::ChunkTaskManager, + state::is_ingame_or_loading, + fixed_timestamp::FixedTimestamp +}; + +mod handshake; +mod world; +mod player; + +pub use handshake::ConnectionRejectionReason; +use handshake::{ + set_client_join_state_to_connected, + say_hello, + check_server_hello_response, + check_server_fuck_off_response, +}; +use world::{ + inject_network_responses_into_manager_queue, + send_block_place_events, + recv_block_place_events, +}; +use player::{ + init_client_map, + send_player_movement_events, + receive_player_movement_events, + receive_player_connect_events +}; + +const NET_TICKRATE: u16 = 33; + +#[derive(Unique, Clone, Copy, PartialEq, Eq)] +pub enum GameType { + Singleplayer, + Muliplayer +} + +#[derive(Unique, Clone, Copy, PartialEq, Eq)] +pub struct ServerAddress(pub SocketAddr); + +#[derive(Unique)] +pub struct UdpClient(pub Client); + +#[derive(Component)] +pub struct NetworkEvent(pub ClientEvent); + +impl NetworkEvent { + ///Checks if postcard-encoded message has a type + pub fn is_message_of_type(&self) -> bool { + let ClientEvent::Receive(data) = &self.0 else { return false }; + if data.len() == 0 { return false } + data[0] == T + } +} + +#[derive(Component)] +pub struct NetworkMessageEvent(pub ServerToClientMessage); + +fn connect_client( + storages: AllStoragesView +) { + log::info!("Creating client"); + let address = storages.borrow::>().unwrap(); + let client = Client::connect(address.0, ClientConfig { + endpoint_config: EndpointConfig { + active_timeout_ms: 10000, + keepalive: true, + keepalive_interval_ms: 5000, + ..Default::default() + }, + }).expect("Client connection failed"); + storages.add_unique(UdpClient(client)); + storages.add_unique(ClientJoinState::Disconnected); +} + +fn poll_client( + mut client: UniqueViewMut, + mut entities: EntitiesViewMut, + mut events: ViewMut, + mut network_events: ViewMut, +) { + entities.bulk_add_entity(( + &mut events, + &mut network_events, + ), client.0.step().map(|event| { + (EventComponent, NetworkEvent(event)) + })); +} + +fn flush_client( + mut client: UniqueViewMut, +) { + client.0.flush(); +} + +fn handle_disconnect( + network_events: View, + mut join_state: UniqueViewMut +) { + for event in network_events.iter() { + if matches!(event.0, ClientEvent::Disconnect) { + log::warn!("Disconnected from server"); + *join_state = ClientJoinState::Disconnected; + return; + } + } +} + +pub fn update_networking() -> Workload { + ( + init_client_map.run_if_missing_unique::(), + connect_client.run_if_missing_unique::(), + poll_client.into_workload().make_fixed(NET_TICKRATE, 0), + ( + set_client_join_state_to_connected, + say_hello, + ).into_sequential_workload().run_if(if_just_connected), + ( + check_server_hello_response, + check_server_fuck_off_response, + handle_disconnect, + ).into_sequential_workload().run_if(is_join_state::<{ClientJoinState::Connected as u8}>), + ( + ( + receive_player_connect_events, + ).into_workload(), + ( + recv_block_place_events, + receive_player_movement_events, + ).into_workload() + ).into_sequential_workload().run_if(is_join_state::<{ClientJoinState::Joined as u8}>).run_if(is_ingame_or_loading), + inject_network_responses_into_manager_queue.run_if(is_ingame_or_loading).skip_if_missing_unique::(), + ).into_sequential_workload() +} + +pub fn update_networking_late() -> Workload { + ( + ( + send_block_place_events, + send_player_movement_events, + ).into_workload().run_if(is_join_state::<{ClientJoinState::Joined as u8}>), + flush_client.into_workload().make_fixed(NET_TICKRATE, 1) + ).into_sequential_workload() +} + +pub fn disconnect_on_exit( + exit: UniqueView, + mut client: UniqueViewMut, +) { + //TODO check if this works + if exit.0 { + if client.0.is_active() { + client.0.flush(); + client.0.disconnect(); + while client.0.is_active() { client.0.step().for_each(|_|()); } + log::info!("Client disconnected"); + } else { + log::info!("Client inactive") + } + } +} + +// conditions + +fn if_just_connected( + network_events: View, +) -> bool { + network_events.iter().any(|event| matches!(&event.0, ClientEvent::Connect)) +} + +fn is_join_state( + join_state: UniqueView +) -> bool { + (*join_state as u8) == STATE +} + +pub fn is_multiplayer( + game_type: UniqueView +) -> bool { + *game_type == GameType::Muliplayer +} + +pub fn is_singleplayer( + game_type: UniqueView +) -> bool { + *game_type == GameType::Singleplayer +} diff --git a/kubi/src/networking/handshake.rs b/kubi/src/networking/handshake.rs index 681a3cf..f610c32 100644 --- a/kubi/src/networking/handshake.rs +++ b/kubi/src/networking/handshake.rs @@ -1,76 +1,111 @@ -use shipyard::{UniqueViewMut, View, IntoIter, AllStoragesViewMut}; -use uflow::{client::Event as ClientEvent, SendMode}; -use kubi_shared::networking::{ - messages::{ClientToServerMessage, ServerToClientMessage, ServerToClientMessageType}, - state::ClientJoinState, - channels::Channel, -}; -use crate::player::{spawn_local_player_multiplayer, spawn_remote_player_multiplayer}; -use super::{UdpClient, NetworkEvent}; - -pub fn set_client_join_state_to_connected( - mut join_state: UniqueViewMut -) { - log::info!("Setting ClientJoinState"); - *join_state = ClientJoinState::Connected; -} - -pub fn say_hello( - mut client: UniqueViewMut, -) { - let username = "XxX-FishFucker-69420-XxX".into(); - let password = None; - log::info!("Authenticating"); - client.0.send( - postcard::to_allocvec( - &ClientToServerMessage::ClientHello { username, password } - ).unwrap().into_boxed_slice(), - Channel::Auth as usize, - SendMode::Reliable - ); -} - -pub fn check_server_hello_response( - mut storages: AllStoragesViewMut, -) { - //Check if we got the message and extract the init data from it - let Some(init) = storages.borrow::>().unwrap().iter().find_map(|event| { - let ClientEvent::Receive(data) = &event.0 else { - return None - }; - if !event.is_message_of_type::<{ServerToClientMessageType::ServerHello as u8}>() { - return None - } - let Ok(parsed_message) = postcard::from_bytes(data) else { - log::error!("Malformed message"); - return None - }; - let ServerToClientMessage::ServerHello { init } = parsed_message else { - unreachable!() - }; - Some(init) - }) else { return }; - - // struct ClientInitData { - // client_id: ClientId, - // username: String, - // position: Vec3, - // velocity: Vec3, - // direction: Quat, - // health: Health, - // } - - //Add components to main player - spawn_local_player_multiplayer(&mut storages, init.user); - - //Init players - for init_data in init.users { - spawn_remote_player_multiplayer(&mut storages, init_data); - } - - // Set state to connected - let mut join_state = storages.borrow::>().unwrap(); - *join_state = ClientJoinState::Joined; - - log::info!("Joined the server!"); -} +use shipyard::{AllStoragesView, AllStoragesViewMut, IntoIter, Unique, UniqueView, UniqueViewMut, View}; +use uflow::{client::Event as ClientEvent, SendMode}; +use kubi_shared::networking::{ + messages::{ClientToServerMessage, ServerToClientMessage, ServerToClientMessageType}, + state::ClientJoinState, + channels::Channel, +}; +use crate::player::{spawn_local_player_multiplayer, spawn_remote_player_multiplayer}; +use super::{UdpClient, NetworkEvent}; + +#[derive(Unique)] +pub struct ConnectionRejectionReason { + pub reason: String, +} + +pub fn set_client_join_state_to_connected( + mut join_state: UniqueViewMut +) { + log::info!("Setting ClientJoinState"); + *join_state = ClientJoinState::Connected; +} + +pub fn say_hello( + mut client: UniqueViewMut, +) { + let username = "XxX-FishFucker-69420-XxX".into(); + let password = None; + log::info!("Authenticating"); + client.0.send( + postcard::to_allocvec( + &ClientToServerMessage::ClientHello { username, password } + ).unwrap().into_boxed_slice(), + Channel::Auth as usize, + SendMode::Reliable + ); +} + +pub fn check_server_hello_response( + mut storages: AllStoragesViewMut, +) { + //Check if we got the message and extract the init data from it + let Some(init) = storages.borrow::>().unwrap().iter().find_map(|event| { + let ClientEvent::Receive(data) = &event.0 else { + return None + }; + if !event.is_message_of_type::<{ServerToClientMessageType::ServerHello as u8}>() { + return None + } + let Ok(parsed_message) = postcard::from_bytes(data) else { + log::error!("Malformed message"); + return None + }; + let ServerToClientMessage::ServerHello { init } = parsed_message else { + unreachable!() + }; + Some(init) + }) else { return }; + + // struct ClientInitData { + // client_id: ClientId, + // username: String, + // position: Vec3, + // velocity: Vec3, + // direction: Quat, + // health: Health, + // } + + //Add components to main player + spawn_local_player_multiplayer(&mut storages, init.user); + + //Init players + for init_data in init.users { + spawn_remote_player_multiplayer(&mut storages, init_data); + } + + // Set state to connected + let mut join_state = storages.borrow::>().unwrap(); + *join_state = ClientJoinState::Joined; + + log::info!("Joined the server!"); +} + +pub fn check_server_fuck_off_response( + storages: AllStoragesView, +) { + //Check if we got the message and extract the init data from it + let Some(reason) = storages.borrow::>().unwrap().iter().find_map(|event| { + let ClientEvent::Receive(data) = &event.0 else { + return None + }; + if !event.is_message_of_type::<{ServerToClientMessageType::ServerFuckOff as u8}>() { + return None + } + let Ok(parsed_message) = postcard::from_bytes(data) else { + log::error!("Malformed message"); + return None + }; + let ServerToClientMessage::ServerFuckOff { reason } = parsed_message else { + unreachable!() + }; + Some(reason) + }) else { return }; + + let mut client = storages.borrow::>().unwrap(); + client.0.disconnect_now(); + + let mut join_state = storages.borrow::>().unwrap(); + *join_state = ClientJoinState::Disconnected; + + storages.add_unique(ConnectionRejectionReason { reason }); +} diff --git a/kubi/src/state.rs b/kubi/src/state.rs index a01d228..fbe8d50 100644 --- a/kubi/src/state.rs +++ b/kubi/src/state.rs @@ -1,58 +1,58 @@ -use shipyard::{Unique, UniqueView, UniqueViewMut, AllStoragesView}; -use std::mem::take; - -#[derive(Unique, PartialEq, Eq, Default, Clone, Copy)] -pub enum GameState { - #[default] - Initial, - Connecting, - LoadingWorld, - InGame -} - -#[derive(Unique, PartialEq, Eq, Default, Clone, Copy)] -pub struct NextState(pub Option); - -pub fn init_state( - all_storages: AllStoragesView, -) { - all_storages.add_unique(GameState::default()); - all_storages.add_unique(NextState::default()); -} - -pub fn update_state( - mut state: UniqueViewMut, - mut next: UniqueViewMut, -) { - *state = take(&mut next.0).unwrap_or(*state); -} - -pub fn is_changing_state( - state: UniqueView -) -> bool { - state.0.is_some() -} - -pub fn is_connecting( - state: UniqueView -) -> bool { - *state == GameState::Connecting -} - -pub fn is_ingame( - state: UniqueView -) -> bool { - *state == GameState::InGame -} - -pub fn is_loading( - state: UniqueView -) -> bool { - matches!(*state, GameState::LoadingWorld) -} - -pub fn is_ingame_or_loading( - state: UniqueView -) -> bool { - matches!(*state, GameState::InGame | GameState::LoadingWorld) -} +use shipyard::{Unique, UniqueView, UniqueViewMut, AllStoragesView}; +use std::mem::take; + +#[derive(Unique, PartialEq, Eq, Default, Clone, Copy)] +pub enum GameState { + #[default] + Initial, + Connecting, + LoadingWorld, + InGame +} + +#[derive(Unique, PartialEq, Eq, Default, Clone, Copy)] +pub struct NextState(pub Option); + +pub fn init_state( + all_storages: AllStoragesView, +) { + all_storages.add_unique(GameState::default()); + all_storages.add_unique(NextState::default()); +} + +pub fn update_state( + mut state: UniqueViewMut, + mut next: UniqueViewMut, +) { + *state = take(&mut next.0).unwrap_or(*state); +} + +pub fn is_changing_state( + state: UniqueView +) -> bool { + state.0.is_some() +} + +pub fn is_connecting( + state: UniqueView +) -> bool { + *state == GameState::Connecting +} + +pub fn is_ingame( + state: UniqueView +) -> bool { + *state == GameState::InGame +} + +pub fn is_loading( + state: UniqueView +) -> bool { + matches!(*state, GameState::LoadingWorld) +} + +pub fn is_ingame_or_loading( + state: UniqueView +) -> bool { + matches!(*state, GameState::InGame | GameState::LoadingWorld) +} diff --git a/kubi/src/ui/connecting_screen.rs b/kubi/src/ui/connecting_screen.rs index ad9a0dc..f40039b 100644 --- a/kubi/src/ui/connecting_screen.rs +++ b/kubi/src/ui/connecting_screen.rs @@ -4,7 +4,7 @@ use shipyard::{IntoWorkload, NonSendSync, UniqueView, UniqueViewMut, Workload}; use crate::{ hui_integration::UiState, loading_screen::loading_screen_base, - networking::ServerAddress, + networking::{ConnectionRejectionReason, ServerAddress}, prefabs::UiFontPrefab, rendering::WindowSize, state::{GameState, NextState} @@ -12,6 +12,8 @@ use crate::{ fn render_connecting_ui( addr: UniqueView, + rejection: Option>, + join_state: UniqueView, mut ui: NonSendSync>, font: UniqueView, size: UniqueView, @@ -19,10 +21,11 @@ fn render_connecting_ui( ui.hui.add( loading_screen_base(vec![ Box::new(Text { - text: format!( - "Connecting to {}...", - addr.0, - ).into(), + text: match (rejection, *join_state) { + (Some(err), _) => format!("Connection rejected by {}\n\n{}", addr.0, err.reason).into(), + (_, ClientJoinState::Disconnected) => format!("Lost connection to {}", addr.0).into(), + _ => format!("Connecting to {}...", addr.0).into(), + }, font: font.0, text_size: 16, ..Default::default()