Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

physnet: connect to instance #116

Merged
merged 2 commits into from
Jun 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 73 additions & 7 deletions apps/networked_physics_demo/client/src/netcode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ impl Plugin for NetcodePlugin {
.add_event::<ConnectToManagerResponse>()
.add_event::<CreateInstanceRequest>()
.add_event::<CreateInstanceResponse>()
.add_event::<ConnectToInstanceRequest>()
.add_event::<ConnectToInstanceResponse>()
.init_resource::<CommandQueueChannel>()
.init_resource::<NetcodeDataModel>()
.add_systems(PreUpdate, (apply_queued_commands, from_data_model))
Expand All @@ -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::<NetcodeManager>),
),
Expand Down Expand Up @@ -80,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<Url>,
/// The URL of the manager to connect to.
pub manager_url: Url,
}

/// Produced in response to [`ConnectToManagerRequest`].
Expand All @@ -91,13 +94,8 @@ pub struct ConnectToManagerResponse(pub Result<()>);
fn handle_connect_to_manager_evt(
command_queue: Res<CommandQueueChannel>,
mut request: EventReader<ConnectToManagerRequest>,
mut response: EventWriter<ConnectToManagerResponse>,
) {
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();
Expand Down Expand Up @@ -175,6 +173,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<Url>,
}

/// Produced in response to [`ConnectToInstanceRequest`].
#[derive(Debug, Event)]
pub struct ConnectToInstanceResponse {
pub result: Result<()>,
/// `None` if local data model.
pub instance_url: Option<Url>,
}

fn handle_connect_to_instance(
command_queue: Res<CommandQueueChannel>,
mut request: EventReader<ConnectToInstanceRequest>,
mut response: EventWriter<ConnectToInstanceResponse>,
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);
Expand Down
115 changes: 99 additions & 16 deletions apps/networked_physics_demo/client/src/title_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,15 @@ use bevy::{
},
system::{Commands, Res, ResMut},
},
input::{keyboard::KeyCode, ButtonInput},
log::trace,
log::{info, trace},
reflect::Reflect,
};
use bevy_inspector_egui::bevy_egui::{egui, EguiContexts};

use crate::{
netcode::{
ConnectToManagerRequest, ConnectToManagerResponse, CreateInstanceRequest,
CreateInstanceResponse,
ConnectToInstanceRequest, ConnectToInstanceResponse, ConnectToManagerRequest,
ConnectToManagerResponse, CreateInstanceRequest, CreateInstanceResponse,
},
AppExt, GameModeState,
};
Expand All @@ -45,6 +44,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))
Expand All @@ -58,14 +58,17 @@ 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::*;

/// [`EventWriter`]s needed by the 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<'_> {
Expand Down Expand Up @@ -93,6 +96,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]
Expand Down Expand Up @@ -166,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,
});
Expand All @@ -175,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");
Expand Down Expand Up @@ -202,7 +218,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()
Expand All @@ -223,24 +242,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();
Expand All @@ -251,7 +298,10 @@ mod ui {
ui.spinner();
self.into()
}
JoinInstance::Connected => todo!(),
JoinInstance::Connected => {
ui.label("Connected!");
self.into()
}
}
}
}
Expand All @@ -260,6 +310,7 @@ mod ui {
fn default() -> Self {
Self::Initial {
instance_url: default(),
error_msg: String::new(),
}
}
}
Expand Down Expand Up @@ -329,11 +380,38 @@ fn handle_create_instance_response(
}
}

fn handle_connect_to_instance_response(
mut ui_state: ResMut<ui::TitleScreen>,
mut connect_to_instance_response: EventReader<ConnectToInstanceResponse>,
) {
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<ui::TitleScreen>,
mut contexts: EguiContexts,
connect_to_manager: EventWriter<ConnectToManagerRequest>,
create_instance: EventWriter<CreateInstanceRequest>,
connect_to_instance: EventWriter<ConnectToInstanceRequest>,
) {
egui::Window::new("Instances")
.resizable(false)
Expand All @@ -344,6 +422,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());
Expand All @@ -353,10 +432,14 @@ fn draw_ui(

/// Emits a state change under certain conditions.
fn should_transition(
kb: Res<ButtonInput<KeyCode>>,
mut connect_to_instance: EventReader<ConnectToInstanceResponse>,
mut next_state: ResMut<NextState<GameModeState>>,
) {
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)
}
}
Loading