From e377e8ff09fd3cd0ee3be44714bf7d303622449a Mon Sep 17 00:00:00 2001 From: Imbris Date: Sun, 14 Apr 2019 23:28:29 +0000 Subject: [PATCH] Add asynchronus client creation to Voxygen Former-commit-id: 0c11cc8a7ddc7b425e036c2357c233e376002c6e --- voxygen/src/menu/main/client_init.rs | 88 ++++++++++++++++++++++++++++ voxygen/src/menu/main/mod.rs | 81 +++++++++++++------------ voxygen/src/menu/main/ui.rs | 76 ++++++++++++++++-------- 3 files changed, 184 insertions(+), 61 deletions(-) create mode 100644 voxygen/src/menu/main/client_init.rs diff --git a/voxygen/src/menu/main/client_init.rs b/voxygen/src/menu/main/client_init.rs new file mode 100644 index 0000000000..f36a12ce68 --- /dev/null +++ b/voxygen/src/menu/main/client_init.rs @@ -0,0 +1,88 @@ +use client::{error::Error as ClientError, Client}; +use common::comp; +use std::{ + sync::mpsc::{channel, Receiver, TryRecvError}, + thread::{self, JoinHandle}, +}; + +#[derive(Debug)] +pub enum Error { + // Error parsing input string or error resolving host name + BadAddress(std::io::Error), + // Parsing yielded an empty iterator (specifically to_socket_addrs()) + NoAddress, + // Parsing/host name resolution successful but could not connect + ConnectionFailed(ClientError), +} + +// Used to asynchronusly parse the server address, resolve host names, and create the client (which involves establishing a connection to the server) +pub struct ClientInit { + rx: Receiver>, +} +impl ClientInit { + pub fn new( + connection_args: (String, u16, bool), + client_args: (comp::Player, Option, u64), + ) -> Self { + let (server_address, default_port, prefer_ipv6) = connection_args; + let (player, character, view_distance) = client_args; + + let (tx, rx) = channel(); + + let handle = Some(thread::spawn(move || { + use std::net::ToSocketAddrs; + // Parses ip address or resolves hostname + // Note: if you use an ipv6 address the number after the last colon will be used as the port unless you use [] around the address + match server_address + .to_socket_addrs() + .or((server_address.as_ref(), default_port).to_socket_addrs()) + { + Ok(socket_adders) => { + let (first_addrs, second_addrs) = + socket_adders.partition::, _>(|a| a.is_ipv6() == prefer_ipv6); + + let mut last_err = None; + + for socket_addr in first_addrs.into_iter().chain(second_addrs) { + match Client::new(socket_addr, player.clone(), character, view_distance) { + Ok(client) => { + tx.send(Ok(client)); + return; + } + Err(err) => { + match err { + // assume connection failed and try next address + ClientError::Network(_) => { + last_err = Some(Error::ConnectionFailed(err)) + } + // TODO: handle error? + _ => panic!( + "Unexpected non-network error when creating client: {:?}", + err + ), + } + } + } + } + // Parsing/host name resolution successful but no connection succeeded + tx.send(Err(last_err.unwrap_or(Error::NoAddress))); + } + Err(err) => { + // Error parsing input string or error resolving host name + tx.send(Err(Error::BadAddress(err))); + } + } + })); + + ClientInit { rx } + } + // Returns None is the thread is still running + // Otherwise returns the Result of client creation + pub fn poll(&self) -> Option> { + match self.rx.try_recv() { + Ok(result) => Some(result), + Err(TryRecvError::Empty) => None, + Err(TryRecvError::Disconnected) => panic!("Thread panicked or already finished"), + } + } +} diff --git a/voxygen/src/menu/main/mod.rs b/voxygen/src/menu/main/mod.rs index 88c08cb0b8..7f12830ac5 100644 --- a/voxygen/src/menu/main/mod.rs +++ b/voxygen/src/menu/main/mod.rs @@ -1,3 +1,4 @@ +mod client_init; mod ui; use super::char_selection::CharSelectionState; @@ -5,11 +6,8 @@ use crate::{ window::{Event, Window}, GlobalState, PlayState, PlayStateResult, }; -use client::{self, Client}; -use common::{ - comp, - clock::Clock, -}; +use client_init::{ClientInit, Error as InitError}; +use common::{clock::Clock, comp}; use std::time::Duration; use ui::{Event as MainMenuEvent, MainMenuUi}; use vek::*; @@ -42,6 +40,9 @@ impl PlayState for MainMenuState { // Set up an fps clock let mut clock = Clock::new(); + // Used for client creation + let mut client_init: Option = None; + loop { // Handle window events for event in global_state.window.fetch_events() { @@ -58,41 +59,45 @@ impl PlayState for MainMenuState { global_state.window.renderer_mut().clear(BG_COLOR); - // Maintain the UI (TODO: Maybe clean this up a little to avoid rightward drift?) - for event in self.main_menu_ui.maintain(global_state.window.renderer_mut()) { + // Poll client creation + match client_init.as_ref().and_then(|init| init.poll()) { + Some(Ok(client)) => { + self.main_menu_ui.connected(); + return PlayStateResult::Push(Box::new(CharSelectionState::new( + &mut global_state.window, + std::rc::Rc::new(std::cell::RefCell::new(client)), + ))); + } + Some(Err(err)) => { + client_init = None; + self.main_menu_ui.login_error(match err { + InitError::BadAddress(_) | InitError::NoAddress => "No such host is known", + InitError::ConnectionFailed(_) => "Could not connect to address", + }.to_string()); + }, + None => {} + } + + // Maintain the UI + for event in self + .main_menu_ui + .maintain(global_state.window.renderer_mut()) + { match event { - MainMenuEvent::LoginAttempt{ username, server_address } => { - use std::net::ToSocketAddrs; + MainMenuEvent::LoginAttempt { + username, + server_address, + } => { const DEFAULT_PORT: u16 = 59003; - // Parses ip address or resolves hostname - // Note: if you use an ipv6 address the number after the last colon will be used as the port unless you use [] around the address - match server_address.to_socket_addrs().or((server_address.as_str(), DEFAULT_PORT).to_socket_addrs()) { - Ok(mut socket_adders) => { - while let Some(socket_addr) = socket_adders.next() { - // TODO: handle error - match Client::new(socket_addr, comp::Player::new(username.clone()), Some(comp::Character::test()), 300) { - Ok(client) => { - return PlayStateResult::Push( - Box::new(CharSelectionState::new( - &mut global_state.window, - std::rc::Rc::new(std::cell::RefCell::new(client)) // <--- TODO: Remove this - )) - ); - } - Err(client::Error::Network(_)) => {} // assume connection failed and try next address - Err(err) => { - panic!("Unexpected non Network error when creating client: {:?}", err); - } - } - } - // Parsing/host name resolution successful but no connection succeeded - self.main_menu_ui.login_error("Could not connect to address".to_string()); - } - Err(err) => { - // Error parsing input string or error resolving host name - self.main_menu_ui.login_error("No such host is known".to_string()); - } - } + // Don't try to connect if there is already a connection in progress + client_init = client_init.or(Some(ClientInit::new( + (server_address, DEFAULT_PORT, false), + ( + comp::Player::new(username.clone()), + Some(comp::Character::test()), + 300, + ), + ))); } MainMenuEvent::Quit => return PlayStateResult::Shutdown, } diff --git a/voxygen/src/menu/main/ui.rs b/voxygen/src/menu/main/ui.rs index 991ad22983..c5e7f83504 100644 --- a/voxygen/src/menu/main/ui.rs +++ b/voxygen/src/menu/main/ui.rs @@ -7,7 +7,7 @@ use common::assets; use conrod_core::{ color::TRANSPARENT, image::Id as ImgId, - position::Dimension, + position::{Dimension, Relative}, text::font::Id as FontId, widget::{text_box::Event as TextBoxEvent, Button, Image, Rectangle, Text, TextBox}, widget_ids, Borderable, Color, Colorable, Labelable, Positionable, Sizeable, Widget, @@ -103,6 +103,7 @@ pub struct MainMenuUi { username: String, server_address: String, login_error: Option, + connecting: Option, } impl MainMenuUi { @@ -138,6 +139,7 @@ impl MainMenuUi { username: "Username".to_string(), server_address: "veloren.mac94.de".to_string(), login_error: None, + connecting: None, } } @@ -154,8 +156,8 @@ impl MainMenuUi { .label("Alpha 0.1") .label_rgba(1.0, 1.0, 1.0, 1.0) .label_font_size(10) - .label_y(conrod_core::position::Relative::Scalar(-40.0)) - .label_x(conrod_core::position::Relative::Scalar(-100.0)) + .label_y(Relative::Scalar(-40.0)) + .label_x(Relative::Scalar(-100.0)) .set(self.ids.v_logo, ui_widgets); // Input fields @@ -163,6 +165,7 @@ impl MainMenuUi { macro_rules! login { () => { self.login_error = None; + self.connecting = Some(std::time::Instant::now()); events.push(Event::LoginAttempt { username: self.username.clone(), server_address: self.server_address.clone(), @@ -241,21 +244,43 @@ impl MainMenuUi { } } // Login button - if Button::image(self.imgs.login_button) - .hover_image(self.imgs.login_button_hover) - .press_image(self.imgs.login_button_press) - .w_h(258.0, 68.0) - .down_from(self.ids.address_bg, 20.0) - .align_middle_x_of(self.ids.address_bg) - .label("Login") - .label_color(TEXT_COLOR) - .label_font_size(24) - .label_y(conrod_core::position::Relative::Scalar(5.0)) - .set(self.ids.login_button, ui_widgets) - .was_clicked() - { - login!(); - } + // Change button text and remove hover/press images if a connection is in progress + if let Some(start) = self.connecting { + Button::image(self.imgs.login_button) + .w_h(258.0, 68.0) + .down_from(self.ids.address_bg, 20.0) + .align_middle_x_of(self.ids.address_bg) + .label("Connecting...") + .label_color({ + let pulse = ((start.elapsed().as_millis() as f32 * 0.008).sin() + 1.0) / 2.0; + Color::Rgba( + TEXT_COLOR.red() * (pulse / 2.0 + 0.5), + TEXT_COLOR.green() * (pulse / 2.0 + 0.5), + TEXT_COLOR.blue() * (pulse / 2.0 + 0.5), + pulse / 4.0 + 0.75, + ) + }) + .label_font_size(24) + .label_y(Relative::Scalar(5.0)) + .set(self.ids.login_button, ui_widgets); + } else { + if Button::image(self.imgs.login_button) + .hover_image(self.imgs.login_button_hover) + .press_image(self.imgs.login_button_press) + .w_h(258.0, 68.0) + .down_from(self.ids.address_bg, 20.0) + .align_middle_x_of(self.ids.address_bg) + .label("Login") + .label_color(TEXT_COLOR) + .label_font_size(24) + .label_y(Relative::Scalar(5.0)) + .set(self.ids.login_button, ui_widgets) + .was_clicked() + { + login!(); + } + }; + // Singleplayer button if Button::image(self.imgs.login_button) .hover_image(self.imgs.login_button_hover) @@ -266,8 +291,8 @@ impl MainMenuUi { .label("Singleplayer") .label_color(TEXT_COLOR) .label_font_size(26) - .label_y(conrod_core::position::Relative::Scalar(5.0)) - .label_x(conrod_core::position::Relative::Scalar(2.0)) + .label_y(Relative::Scalar(5.0)) + .label_x(Relative::Scalar(2.0)) .set(self.ids.singleplayer_button, ui_widgets) .was_clicked() { @@ -282,7 +307,7 @@ impl MainMenuUi { .label("Quit") .label_color(TEXT_COLOR) .label_font_size(20) - .label_y(conrod_core::position::Relative::Scalar(3.0)) + .label_y(Relative::Scalar(3.0)) .set(self.ids.quit_button, ui_widgets) .was_clicked() { @@ -297,7 +322,7 @@ impl MainMenuUi { .label("Settings") .label_color(TEXT_COLOR) .label_font_size(20) - .label_y(conrod_core::position::Relative::Scalar(3.0)) + .label_y(Relative::Scalar(3.0)) .set(self.ids.settings_button, ui_widgets) .was_clicked() {}; @@ -310,7 +335,7 @@ impl MainMenuUi { .label("Servers") .label_color(TEXT_COLOR) .label_font_size(20) - .label_y(conrod_core::position::Relative::Scalar(3.0)) + .label_y(Relative::Scalar(3.0)) .set(self.ids.servers_button, ui_widgets) .was_clicked() {}; @@ -320,6 +345,11 @@ impl MainMenuUi { pub fn login_error(&mut self, msg: String) { self.login_error = Some(msg); + self.connecting = None; + } + + pub fn connected(&mut self) { + self.connecting = None; } pub fn handle_event(&mut self, event: ui::Event) {