diff --git a/assets/voxygen/i18n/en/char_selection.ron b/assets/voxygen/i18n/en/char_selection.ron index a078c83a81..7f60914312 100644 --- a/assets/voxygen/i18n/en/char_selection.ron +++ b/assets/voxygen/i18n/en/char_selection.ron @@ -24,6 +24,7 @@ "char_selection.eyeshape": "Eye Details", "char_selection.accessories": "Accessories", "char_selection.create_info_name": "Your Character needs a name!", + "char_selection.version_mismatch": "WARNING! This server is running a different, possibly incompatible game version. Please update your game." }, vector_map: { diff --git a/assets/voxygen/i18n/en/main.ron b/assets/voxygen/i18n/en/main.ron index ab22a8ac9d..6bf3f4da12 100644 --- a/assets/voxygen/i18n/en/main.ron +++ b/assets/voxygen/i18n/en/main.ron @@ -50,7 +50,7 @@ https://veloren.net/account/."#, "main.login.timeout": "Timeout: Server did not respond in time. (Overloaded or network issues).", "main.login.server_shut_down": "Server shut down", "main.login.network_error": "Network error", - "main.login.network_wrong_version": "The server is running a different version than you are. Check your version and update your game.", + "main.login.network_wrong_version": "Mismatched server and client version, please update your game client.", "main.login.failed_sending_request": "Request to Auth server failed", "main.login.invalid_character": "The selected character is invalid", "main.login.client_crashed": "Client crashed", @@ -58,7 +58,8 @@ https://veloren.net/account/."#, "main.login.banned": "You have been banned with the following reason", "main.login.kicked": "You have been kicked with the following reason", "main.login.select_language": "Select a language", - + "main.login.client_version": "Client Version", + "main.login.server_version": "Server Version", "main.servers.select_server": "Select a server", /// End Main screen section }, diff --git a/client/examples/chat-cli/main.rs b/client/examples/chat-cli/main.rs index 9266ac2932..2d7f7f210e 100644 --- a/client/examples/chat-cli/main.rs +++ b/client/examples/chat-cli/main.rs @@ -52,7 +52,7 @@ fn main() { let addr = ConnectionArgs::resolve(&server_addr, false) .await .expect("dns resolve failed"); - Client::new(addr, None, runtime2).await + Client::new(addr, None, runtime2, &mut None).await }) .expect("Failed to create client instance"); diff --git a/client/src/bin/bot/main.rs b/client/src/bin/bot/main.rs index a9ae28f1e8..7cb0aaab38 100644 --- a/client/src/bin/bot/main.rs +++ b/client/src/bin/bot/main.rs @@ -59,7 +59,7 @@ pub fn make_client(runtime: &Arc, server: &str) -> Client { let connection_args = ConnectionArgs::resolve(server, false) .await .expect("DNS resolution failed"); - Client::new(connection_args, view_distance, runtime2) + Client::new(connection_args, view_distance, runtime2, &mut None) .await .expect("Failed to connect to server") }) diff --git a/client/src/lib.rs b/client/src/lib.rs index 63168d9337..a9e1fc582a 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -9,6 +9,7 @@ pub mod error; // Reexports pub use crate::error::Error; pub use authc::AuthClientError; +pub use common_net::msg::ServerInfo; pub use specs::{ join::Join, saveload::{Marker, MarkerAllocator}, @@ -49,7 +50,7 @@ use common_net::{ world_msg::{EconomyInfo, SiteId, SiteInfo}, ChatMsgValidationError, ClientGeneral, ClientMsg, ClientRegister, ClientType, DisconnectReason, InviteAnswer, Notification, PingMsg, PlayerInfo, PlayerListUpdate, - PresenceKind, RegisterError, ServerGeneral, ServerInfo, ServerInit, ServerRegisterAnswer, + PresenceKind, RegisterError, ServerGeneral, ServerInit, ServerRegisterAnswer, MAX_BYTES_CHAT_MSG, }, sync::WorldSyncExt, @@ -66,6 +67,7 @@ use rayon::prelude::*; use specs::Component; use std::{ collections::{BTreeMap, VecDeque}, + mem, sync::Arc, time::{Duration, Instant}, }; @@ -204,6 +206,8 @@ impl Client { addr: ConnectionArgs, view_distance: Option, runtime: Arc, + // TODO: refactor to avoid needing to use this out parameter + mismatched_server_info: &mut Option, ) -> Result { let network = Network::new(Pid::new(), &runtime); @@ -235,8 +239,6 @@ impl Client { register_stream.send(ClientType::Game)?; let server_info: ServerInfo = register_stream.recv().await?; - - // TODO: Display that versions don't match in Voxygen if server_info.git_hash != *common::util::GIT_HASH { warn!( "Server is running {}[{}], you are running {}[{}], versions might be incompatible!", @@ -245,6 +247,10 @@ impl Client { common::util::GIT_HASH.to_string(), common::util::GIT_DATE.to_string(), ); + + // Pass the server info back to the caller to ensure they can access it even + // if this function errors. + mem::swap(mismatched_server_info, &mut Some(server_info.clone())); } debug!("Auth Server: {:?}", server_info.auth_provider); @@ -2449,6 +2455,7 @@ mod tests { ConnectionArgs::IpAndPort(vec![socket]), view_distance, runtime2, + &mut None, )); let _ = veloren_client.map(|mut client| { diff --git a/network/src/api.rs b/network/src/api.rs index 1692e4b87c..e04094d6ce 100644 --- a/network/src/api.rs +++ b/network/src/api.rs @@ -1055,7 +1055,7 @@ where handle.spawn(async move { match finished_receiver.await { Ok(data) => f(data), - Err(e) => panic!("{}{}: {}", name, CHANNEL_ERR, e), + Err(e) => error!("{}{}: {}", name, CHANNEL_ERR, e), } }); } else { diff --git a/voxygen/src/menu/char_selection/ui/mod.rs b/voxygen/src/menu/char_selection/ui/mod.rs index 67059d3d16..9eac601f9d 100644 --- a/voxygen/src/menu/char_selection/ui/mod.rs +++ b/voxygen/src/menu/char_selection/ui/mod.rs @@ -20,7 +20,7 @@ use crate::{ }, window, GlobalState, }; -use client::Client; +use client::{Client, ServerInfo}; use common::{ assets::AssetHandle, character::{CharacterId, CharacterItem, MAX_CHARACTERS_PER_PLAYER, MAX_NAME_LENGTH}, @@ -223,7 +223,7 @@ struct Controls { version: String, // Alpha disclaimer alpha: String, - + server_mismatched_version: Option, tooltip_manager: TooltipManager, // Zone for rotating the character with the mouse mouse_detector: mouse_detector::State, @@ -264,16 +264,25 @@ enum Message { } impl Controls { - fn new(fonts: Fonts, imgs: Imgs, selected: Option, default_name: String) -> Self { + fn new( + fonts: Fonts, + imgs: Imgs, + selected: Option, + default_name: String, + server_info: &ServerInfo, + ) -> Self { let version = common::util::DISPLAY_VERSION_LONG.clone(); let alpha = format!("Veloren {}", common::util::DISPLAY_VERSION.as_str()); + let server_mismatched_version = (common::util::GIT_HASH.to_string() + != server_info.git_hash) + .then(|| server_info.git_hash.clone()); Self { fonts, imgs, version, alpha, - + server_mismatched_version, tooltip_manager: TooltipManager::new(TOOLTIP_HOVER_DUR, TOOLTIP_FADE_DUR), mouse_detector: Default::default(), mode: Mode::select(Some(InfoContent::LoadingCharacters)), @@ -1210,14 +1219,46 @@ impl Controls { }, }; - Container::new( - Column::with_children(vec![top_text.into(), content]) - .spacing(3) - .width(Length::Fill) - .height(Length::Fill), - ) - .padding(3) - .into() + // TODO: There is probably a better way to conditionally add in the warning box + // here + if let Some(mismatched_version) = &self.server_mismatched_version { + let warning = iced::Text::::new(format!( + "{}\n{}: {} {}: {}", + i18n.get("char_selection.version_mismatch"), + i18n.get("main.login.server_version"), + mismatched_version, + i18n.get("main.login.client_version"), + common::util::GIT_HASH.to_string() + )) + .size(self.fonts.cyri.scale(18)) + .color(iced::Color::from_rgb(1.0, 0.0, 0.0)) + .width(Length::Fill) + .horizontal_alignment(HorizontalAlignment::Center); + let warning_container = + Container::new(Row::with_children(vec![warning.into()]).width(Length::Fill)) + .style(style::container::Style::color(Rgba::new(0, 0, 0, 217))) + .padding(12) + .center_x() + .width(Length::Fill); + + Container::new( + Column::with_children(vec![top_text.into(), warning_container.into(), content]) + .spacing(3) + .width(Length::Fill) + .height(Length::Fill), + ) + .padding(3) + .into() + } else { + Container::new( + Column::with_children(vec![top_text.into(), content]) + .spacing(3) + .width(Length::Fill) + .height(Length::Fill), + ) + .padding(3) + .into() + } } fn update(&mut self, message: Message, events: &mut Vec, characters: &[CharacterItem]) { @@ -1451,6 +1492,7 @@ impl CharSelectionUi { Imgs::load(&mut ui).expect("Failed to load images"), selected_character, default_name, + client.server_info(), ); Self { diff --git a/voxygen/src/menu/main/client_init.rs b/voxygen/src/menu/main/client_init.rs index 7111b6467c..6cbe4ee2bc 100644 --- a/voxygen/src/menu/main/client_init.rs +++ b/voxygen/src/menu/main/client_init.rs @@ -1,7 +1,7 @@ use client::{ addr::ConnectionArgs, error::{Error as ClientError, NetworkConnectError, NetworkError}, - Client, + Client, ServerInfo, }; use crossbeam::channel::{unbounded, Receiver, Sender, TryRecvError}; use std::{ @@ -17,7 +17,10 @@ use tracing::{trace, warn}; #[derive(Debug)] pub enum Error { NoAddress, - ClientError(ClientError), + ClientError { + error: ClientError, + mismatched_server_info: Option, + }, ClientCrashed, } @@ -103,16 +106,21 @@ impl ClientInit { if cancel2.load(Ordering::Relaxed) { break; } + let mut mismatched_server_info = None; match Client::new( connection_args.clone(), view_distance, Arc::clone(&runtime2), + &mut mismatched_server_info, ) .await { Ok(mut client) => { if let Err(e) = client.register(username, password, trust_fn).await { - last_err = Some(Error::ClientError(e)); + last_err = Some(Error::ClientError { + error: e, + mismatched_server_info: None, + }); break 'tries; } let _ = tx.send(Msg::Done(Ok(client))); @@ -126,7 +134,10 @@ impl ClientInit { }, Err(e) => { trace!(?e, "Aborting server connection attempt"); - last_err = Some(Error::ClientError(e)); + last_err = Some(Error::ClientError { + error: e, + mismatched_server_info, + }); break 'tries; }, } diff --git a/voxygen/src/menu/main/mod.rs b/voxygen/src/menu/main/mod.rs index 75d1dfb516..f9d0bbd287 100644 --- a/voxygen/src/menu/main/mod.rs +++ b/voxygen/src/menu/main/mod.rs @@ -14,12 +14,15 @@ use crate::{ }; #[cfg(feature = "singleplayer")] use client::addr::ConnectionArgs; -use client::error::{InitProtocolError, NetworkConnectError, NetworkError}; +use client::{ + error::{InitProtocolError, NetworkConnectError, NetworkError}, + ServerInfo, +}; use client_init::{ClientConnArgs, ClientInit, Error as InitError, Msg as InitMsg}; use common::{assets::AssetExt, comp}; use common_base::span; use scene::Scene; -use std::sync::Arc; +use std::{fmt::Debug, sync::Arc}; use tokio::runtime; use tracing::error; use ui::{Event as MainMenuEvent, MainMenuUi}; @@ -129,7 +132,10 @@ impl PlayState for MainMenuState { InitError::NoAddress => { localized_strings.get("main.login.server_not_found").into() }, - InitError::ClientError(err) => match err { + InitError::ClientError { + error, + mismatched_server_info, + } => match error { client::Error::SpecsErr(e) => format!( "{}: {}", localized_strings.get("main.login.internal_error"), @@ -169,23 +175,25 @@ impl PlayState for MainMenuState { }, client::Error::NetworkErr(NetworkError::ConnectFailed( NetworkConnectError::Handshake(InitProtocolError::WrongVersion(_)), - )) => localized_strings - .get("main.login.network_wrong_version") - .into(), - client::Error::NetworkErr(e) => format!( - "{}: {:?}", - localized_strings.get("main.login.network_error"), - e + )) => get_network_error_text( + &localized_strings, + localized_strings.get("main.login.network_wrong_version"), + mismatched_server_info, ), - client::Error::ParticipantErr(e) => format!( - "{}: {:?}", - localized_strings.get("main.login.network_error"), - e + client::Error::NetworkErr(e) => get_network_error_text( + &localized_strings, + e, + mismatched_server_info, ), - client::Error::StreamErr(e) => format!( - "{}: {:?}", - localized_strings.get("main.login.network_error"), - e + client::Error::ParticipantErr(e) => get_network_error_text( + &localized_strings, + e, + mismatched_server_info, + ), + client::Error::StreamErr(e) => get_network_error_text( + &localized_strings, + e, + mismatched_server_info, ), client::Error::Other(e) => { format!("{}: {}", localized_strings.get("common.error"), e) @@ -349,6 +357,31 @@ impl PlayState for MainMenuState { } } +/// When a network error is received and there is a mismatch between the client +/// and server version it is almost definitely due to this mismatch rather than +/// a true networking error. +fn get_network_error_text( + localization: &Localization, + error: impl Debug, + mismatched_server_info: Option, +) -> String { + if let Some(server_info) = mismatched_server_info { + format!( + "{} {}: {} {}: {}", + localization.get("main.login.network_wrong_version"), + localization.get("main.login.client_version"), + common::util::GIT_HASH.to_string(), + localization.get("main.login.server_version"), + server_info.git_hash + ) + } else { + format!( + "{}: {:?}", + localization.get("main.login.network_error"), + error + ) + } +} fn attempt_login( settings: &mut Settings, info_message: &mut Option,