From cd8589e908a34800b5f7974cb0365654d2cf41a7 Mon Sep 17 00:00:00 2001 From: Ryan Butler Date: Fri, 21 Jun 2024 04:56:37 -0400 Subject: [PATCH 1/2] physnet: connect to instance --- .../client/src/netcode.rs | 71 ++++++++++++ .../client/src/title_screen.rs | 107 +++++++++++++++--- 2 files changed, 164 insertions(+), 14 deletions(-) diff --git a/apps/networked_physics_demo/client/src/netcode.rs b/apps/networked_physics_demo/client/src/netcode.rs index 798b5ae..3403d6b 100644 --- a/apps/networked_physics_demo/client/src/netcode.rs +++ b/apps/networked_physics_demo/client/src/netcode.rs @@ -36,6 +36,8 @@ impl Plugin for NetcodePlugin { .add_event::() .add_event::() .add_event::() + .add_event::() + .add_event::() .init_resource::() .init_resource::() .add_systems(PreUpdate, (apply_queued_commands, from_data_model)) @@ -44,6 +46,7 @@ impl Plugin for NetcodePlugin { Update, ( handle_connect_to_manager_evt, + handle_connect_to_instance, handle_create_instance_evt .run_if(resource_exists::), ), @@ -175,6 +178,74 @@ fn handle_create_instance_evt( } } +/// Other plugins can send this to create and then connect to a new instance. +#[derive(Debug, Event, Eq, PartialEq)] +pub struct ConnectToInstanceRequest { + /// If `None`, use `DmMode::Local`. + pub instance_url: Option, +} + +/// Produced in response to [`ConnectToInstanceRequest`]. +#[derive(Debug, Event)] +pub struct ConnectToInstanceResponse { + pub result: Result<()>, + /// `None` if local data model. + pub instance_url: Option, +} + +fn handle_connect_to_instance( + command_queue: Res, + mut request: EventReader, + mut response: EventWriter, + mut commands: Commands, +) { + for ConnectToInstanceRequest { instance_url } in request.read() { + let Some(instance_url) = instance_url else { + commands.insert_resource(NetcodeDataModel { + dm: DmEnum::Local(DataModel::new()), + }); + response.send(ConnectToInstanceResponse { + result: Ok(()), + instance_url: None, + }); + continue; + }; + let tx = command_queue.tx.clone(); + let pool = IoTaskPool::get(); + let url = instance_url.clone(); + debug!("spawned async task for connecting to instance"); + pool.spawn(async_compat::Compat::new(async move { + let connect_result = + replicate_client::instance::Instance::connect(url.clone(), None) + .await + .wrap_err("failed to connect to instance"); + if let Err(ref err) = connect_result { + error!("{err:?}"); + } + + // We use a command queue to enqueue commands back to bevy from the + // async code. + let mut queue = CommandQueue::default(); + let connect_result = connect_result.map(|instance| { + queue.push(|w: &mut World| { + w.insert_resource(NetcodeDataModel { + dm: DmEnum::Remote(instance), + }); + }); + }); + queue.push(|w: &mut World| { + w.send_event(ConnectToInstanceResponse { + result: connect_result, + instance_url: Some(url), + }) + .expect("failed to send event"); + }); + let _ = tx.send(queue).await; + })) + .detach() + } +} + /// Add this to entities that should be synchronized over the network #[derive(Debug, Eq, PartialEq, Component)] pub struct Synchronized(pub DmEntity); diff --git a/apps/networked_physics_demo/client/src/title_screen.rs b/apps/networked_physics_demo/client/src/title_screen.rs index 4332a75..1526ee8 100644 --- a/apps/networked_physics_demo/client/src/title_screen.rs +++ b/apps/networked_physics_demo/client/src/title_screen.rs @@ -11,15 +11,17 @@ use bevy::{ system::{Commands, Res, ResMut}, }, input::{keyboard::KeyCode, ButtonInput}, - log::trace, + log::{info, trace}, + prelude::default, reflect::Reflect, }; use bevy_inspector_egui::bevy_egui::{egui, EguiContexts}; use crate::{ netcode::{ - ConnectToManagerRequest, ConnectToManagerResponse, CreateInstanceRequest, - CreateInstanceResponse, + ConnectToInstanceRequest, ConnectToInstanceResponse, ConnectToManagerRequest, + ConnectToManagerResponse, CreateInstanceRequest, CreateInstanceResponse, + NetcodeDataModel, }, AppExt, GameModeState, }; @@ -45,6 +47,7 @@ impl Plugin for TitleScreenPlugin { ( handle_connect_to_manager_response, handle_create_instance_response, + handle_connect_to_instance_response, ) .in_set(UiStateSystems), (should_transition, draw_ui.after(UiStateSystems)) @@ -58,7 +61,9 @@ impl Plugin for TitleScreenPlugin { mod ui { use bevy::{ecs::system::Resource, prelude::default}; - use crate::netcode::{ConnectToManagerRequest, CreateInstanceRequest}; + use crate::netcode::{ + ConnectToInstanceRequest, ConnectToManagerRequest, CreateInstanceRequest, + }; use super::*; @@ -66,6 +71,7 @@ mod ui { pub(super) struct EventWriters<'a> { pub connect_to_manager: EventWriter<'a, ConnectToManagerRequest>, pub create_instance: EventWriter<'a, CreateInstanceRequest>, + pub connect_to_instance: EventWriter<'a, ConnectToInstanceRequest>, } impl EventWriters<'_> { @@ -93,6 +99,12 @@ mod ui { } } + impl UiEvent for ConnectToInstanceRequest { + fn send(self, evw: &mut EventWriters<'_>) { + evw.connect_to_instance.send(self); + } + } + #[derive(Debug, Default, Resource, Reflect, Eq, PartialEq, derive_more::From)] pub enum TitleScreen { #[default] @@ -202,7 +214,10 @@ mod ui { ui.label(&*instance_url); ui.output_mut(|o| instance_url.clone_into(&mut o.copied_text)); if ui.button("Join Instance").clicked() { - // TODO: Spawn event to connect to instance + let instance_url = instance_url.parse().expect("infallible"); + evw.send(ConnectToInstanceRequest { + instance_url: Some(instance_url), + }); return JoinInstance::WaitingForConnection.into(); } self.into() @@ -223,24 +238,52 @@ mod ui { /// User has chosen to join an instance. #[derive(Debug, Reflect, Eq, PartialEq)] pub enum JoinInstance { - Initial { instance_url: String }, + Initial { + instance_url: String, + error_msg: String, + }, WaitingForConnection, Connected, } impl JoinInstance { - fn draw(mut self, ui: &mut egui::Ui, _evw: EventWriters) -> TitleScreen { + fn draw(mut self, ui: &mut egui::Ui, mut evw: EventWriters) -> TitleScreen { match self { JoinInstance::Initial { ref mut instance_url, + ref mut error_msg, } => { ui.add( egui::TextEdit::singleline(instance_url) - .hint_text("Instance Url"), + .hint_text("Instance Url") + .text_color_opt( + (!error_msg.is_empty()).then_some(egui::Color32::RED), + ), ); - if ui.button("Submit").clicked() { - // TODO: spawn event for joining instance - return JoinInstance::WaitingForConnection.into(); + if instance_url.is_empty() { + error_msg.clear(); + } + let text = if error_msg.is_empty() { + "Connect" + } else { + error_msg.as_str() + }; + if ui + .add_enabled(!instance_url.is_empty(), egui::Button::new(text)) + .clicked() + { + match instance_url.parse() { + Ok(instance_url) => { + evw.send(ConnectToInstanceRequest { + instance_url: Some(instance_url), + }); + return JoinInstance::WaitingForConnection.into(); + } + Err(_parse_err) => { + error_msg.clear(); + error_msg.push_str("Invalid URL"); + } + } } if ui.button("Back").clicked() { return default(); @@ -251,7 +294,10 @@ mod ui { ui.spinner(); self.into() } - JoinInstance::Connected => todo!(), + JoinInstance::Connected => { + ui.label("Connected!"); + self.into() + } } } } @@ -260,6 +306,7 @@ mod ui { fn default() -> Self { Self::Initial { instance_url: default(), + error_msg: String::new(), } } } @@ -329,11 +376,38 @@ fn handle_create_instance_response( } } +fn handle_connect_to_instance_response( + mut ui_state: ResMut, + mut connect_to_instance_response: EventReader, +) { + let ui::TitleScreen::Join(ref mut join_state) = *ui_state else { + return; + }; + for response in connect_to_instance_response.read() { + trace!("handling ConnectToInstanceResponse"); + match &response.result { + Err(_err) => { + *join_state = ui::JoinInstance::Initial { + error_msg: "Failed to connect to instance".to_owned(), + instance_url: response + .instance_url + .as_ref() + .map(|url| url.to_string()) + .unwrap_or_default(), + } + } + Ok(_) => *join_state = ui::JoinInstance::Connected, + } + // + } +} + fn draw_ui( mut state: ResMut, mut contexts: EguiContexts, connect_to_manager: EventWriter, create_instance: EventWriter, + connect_to_instance: EventWriter, ) { egui::Window::new("Instances") .resizable(false) @@ -344,6 +418,7 @@ fn draw_ui( let evw = EventWriters { connect_to_manager, create_instance, + connect_to_instance, }; // need ownership of state, so replace with the default temporarily let stolen = std::mem::take(state.as_mut()); @@ -353,10 +428,14 @@ fn draw_ui( /// Emits a state change under certain conditions. fn should_transition( - kb: Res>, + mut connect_to_instance: EventReader, mut next_state: ResMut>, ) { - if kb.just_pressed(KeyCode::Space) { + if connect_to_instance + .read() + .any(|response| response.result.is_ok()) + { + info!("Connected to instance, transitioning..."); next_state.set(GameModeState::InMinecraft) } } From e75cb3b4452912fc02fc6b43633874218a4a8524 Mon Sep 17 00:00:00 2001 From: Ryan Butler Date: Fri, 21 Jun 2024 05:16:03 -0400 Subject: [PATCH 2/2] fix self hosting --- apps/networked_physics_demo/client/src/netcode.rs | 9 ++------- .../client/src/title_screen.rs | 12 ++++++++---- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/apps/networked_physics_demo/client/src/netcode.rs b/apps/networked_physics_demo/client/src/netcode.rs index 3403d6b..4e4ec84 100644 --- a/apps/networked_physics_demo/client/src/netcode.rs +++ b/apps/networked_physics_demo/client/src/netcode.rs @@ -83,8 +83,8 @@ fn apply_queued_commands( /// Other plugins create this when they want to connect to a manager. #[derive(Debug, Event, Eq, PartialEq)] pub struct ConnectToManagerRequest { - /// The URL of the manager to connect to. If `None`, locally host. - pub manager_url: Option, + /// The URL of the manager to connect to. + pub manager_url: Url, } /// Produced in response to [`ConnectToManagerRequest`]. @@ -94,13 +94,8 @@ pub struct ConnectToManagerResponse(pub Result<()>); fn handle_connect_to_manager_evt( command_queue: Res, mut request: EventReader, - mut response: EventWriter, ) { for ConnectToManagerRequest { manager_url } in request.read() { - let Some(manager_url) = manager_url else { - response.send(ConnectToManagerResponse(Ok(()))); - continue; - }; let manager_url = manager_url.to_owned(); let tx = command_queue.tx.clone(); let pool = IoTaskPool::get(); diff --git a/apps/networked_physics_demo/client/src/title_screen.rs b/apps/networked_physics_demo/client/src/title_screen.rs index 1526ee8..53d6911 100644 --- a/apps/networked_physics_demo/client/src/title_screen.rs +++ b/apps/networked_physics_demo/client/src/title_screen.rs @@ -10,9 +10,7 @@ use bevy::{ }, system::{Commands, Res, ResMut}, }, - input::{keyboard::KeyCode, ButtonInput}, log::{info, trace}, - prelude::default, reflect::Reflect, }; use bevy_inspector_egui::bevy_egui::{egui, EguiContexts}; @@ -21,7 +19,6 @@ use crate::{ netcode::{ ConnectToInstanceRequest, ConnectToInstanceResponse, ConnectToManagerRequest, ConnectToManagerResponse, CreateInstanceRequest, CreateInstanceResponse, - NetcodeDataModel, }, AppExt, GameModeState, }; @@ -178,7 +175,7 @@ mod ui { .then(|| manager_url.parse()) .transpose() { - Ok(parsed_manager_url) => { + Ok(Some(parsed_manager_url)) => { evw.send(ConnectToManagerRequest { manager_url: parsed_manager_url, }); @@ -187,6 +184,13 @@ mod ui { } .into(); } + Ok(None) => { + // Locally host + evw.send(ConnectToInstanceRequest { + instance_url: None, + }); + return JoinInstance::WaitingForConnection.into(); + } Err(_parse_err) => { error_msg.clear(); error_msg.push_str("Invalid URL");