Added client/server version mismatch message when a network error is encountered during client init.

Added warning banner on character select when successfully connected to a server with a mismatched version.
This commit is contained in:
Ben Wallis 2021-04-22 18:27:38 +01:00 committed by Avi Weinstock
parent 53025a861e
commit 76daf7a395
9 changed files with 137 additions and 42 deletions

View File

@ -24,6 +24,7 @@
"char_selection.eyeshape": "Eye Details", "char_selection.eyeshape": "Eye Details",
"char_selection.accessories": "Accessories", "char_selection.accessories": "Accessories",
"char_selection.create_info_name": "Your Character needs a name!", "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: { vector_map: {

View File

@ -50,7 +50,7 @@ https://veloren.net/account/."#,
"main.login.timeout": "Timeout: Server did not respond in time. (Overloaded or network issues).", "main.login.timeout": "Timeout: Server did not respond in time. (Overloaded or network issues).",
"main.login.server_shut_down": "Server shut down", "main.login.server_shut_down": "Server shut down",
"main.login.network_error": "Network error", "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.failed_sending_request": "Request to Auth server failed",
"main.login.invalid_character": "The selected character is invalid", "main.login.invalid_character": "The selected character is invalid",
"main.login.client_crashed": "Client crashed", "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.banned": "You have been banned with the following reason",
"main.login.kicked": "You have been kicked with the following reason", "main.login.kicked": "You have been kicked with the following reason",
"main.login.select_language": "Select a language", "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", "main.servers.select_server": "Select a server",
/// End Main screen section /// End Main screen section
}, },

View File

@ -52,7 +52,7 @@ fn main() {
let addr = ConnectionArgs::resolve(&server_addr, false) let addr = ConnectionArgs::resolve(&server_addr, false)
.await .await
.expect("dns resolve failed"); .expect("dns resolve failed");
Client::new(addr, None, runtime2).await Client::new(addr, None, runtime2, &mut None).await
}) })
.expect("Failed to create client instance"); .expect("Failed to create client instance");

View File

@ -59,7 +59,7 @@ pub fn make_client(runtime: &Arc<Runtime>, server: &str) -> Client {
let connection_args = ConnectionArgs::resolve(server, false) let connection_args = ConnectionArgs::resolve(server, false)
.await .await
.expect("DNS resolution failed"); .expect("DNS resolution failed");
Client::new(connection_args, view_distance, runtime2) Client::new(connection_args, view_distance, runtime2, &mut None)
.await .await
.expect("Failed to connect to server") .expect("Failed to connect to server")
}) })

View File

@ -9,6 +9,7 @@ pub mod error;
// Reexports // Reexports
pub use crate::error::Error; pub use crate::error::Error;
pub use authc::AuthClientError; pub use authc::AuthClientError;
pub use common_net::msg::ServerInfo;
pub use specs::{ pub use specs::{
join::Join, join::Join,
saveload::{Marker, MarkerAllocator}, saveload::{Marker, MarkerAllocator},
@ -49,7 +50,7 @@ use common_net::{
world_msg::{EconomyInfo, SiteId, SiteInfo}, world_msg::{EconomyInfo, SiteId, SiteInfo},
ChatMsgValidationError, ClientGeneral, ClientMsg, ClientRegister, ClientType, ChatMsgValidationError, ClientGeneral, ClientMsg, ClientRegister, ClientType,
DisconnectReason, InviteAnswer, Notification, PingMsg, PlayerInfo, PlayerListUpdate, DisconnectReason, InviteAnswer, Notification, PingMsg, PlayerInfo, PlayerListUpdate,
PresenceKind, RegisterError, ServerGeneral, ServerInfo, ServerInit, ServerRegisterAnswer, PresenceKind, RegisterError, ServerGeneral, ServerInit, ServerRegisterAnswer,
MAX_BYTES_CHAT_MSG, MAX_BYTES_CHAT_MSG,
}, },
sync::WorldSyncExt, sync::WorldSyncExt,
@ -66,6 +67,7 @@ use rayon::prelude::*;
use specs::Component; use specs::Component;
use std::{ use std::{
collections::{BTreeMap, VecDeque}, collections::{BTreeMap, VecDeque},
mem,
sync::Arc, sync::Arc,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
@ -204,6 +206,8 @@ impl Client {
addr: ConnectionArgs, addr: ConnectionArgs,
view_distance: Option<u32>, view_distance: Option<u32>,
runtime: Arc<Runtime>, runtime: Arc<Runtime>,
// TODO: refactor to avoid needing to use this out parameter
mismatched_server_info: &mut Option<ServerInfo>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let network = Network::new(Pid::new(), &runtime); let network = Network::new(Pid::new(), &runtime);
@ -235,8 +239,6 @@ impl Client {
register_stream.send(ClientType::Game)?; register_stream.send(ClientType::Game)?;
let server_info: ServerInfo = register_stream.recv().await?; 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 { if server_info.git_hash != *common::util::GIT_HASH {
warn!( warn!(
"Server is running {}[{}], you are running {}[{}], versions might be incompatible!", "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_HASH.to_string(),
common::util::GIT_DATE.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); debug!("Auth Server: {:?}", server_info.auth_provider);
@ -2449,6 +2455,7 @@ mod tests {
ConnectionArgs::IpAndPort(vec![socket]), ConnectionArgs::IpAndPort(vec![socket]),
view_distance, view_distance,
runtime2, runtime2,
&mut None,
)); ));
let _ = veloren_client.map(|mut client| { let _ = veloren_client.map(|mut client| {

View File

@ -1055,7 +1055,7 @@ where
handle.spawn(async move { handle.spawn(async move {
match finished_receiver.await { match finished_receiver.await {
Ok(data) => f(data), Ok(data) => f(data),
Err(e) => panic!("{}{}: {}", name, CHANNEL_ERR, e), Err(e) => error!("{}{}: {}", name, CHANNEL_ERR, e),
} }
}); });
} else { } else {

View File

@ -20,7 +20,7 @@ use crate::{
}, },
window, GlobalState, window, GlobalState,
}; };
use client::Client; use client::{Client, ServerInfo};
use common::{ use common::{
assets::AssetHandle, assets::AssetHandle,
character::{CharacterId, CharacterItem, MAX_CHARACTERS_PER_PLAYER, MAX_NAME_LENGTH}, character::{CharacterId, CharacterItem, MAX_CHARACTERS_PER_PLAYER, MAX_NAME_LENGTH},
@ -223,7 +223,7 @@ struct Controls {
version: String, version: String,
// Alpha disclaimer // Alpha disclaimer
alpha: String, alpha: String,
server_mismatched_version: Option<String>,
tooltip_manager: TooltipManager, tooltip_manager: TooltipManager,
// Zone for rotating the character with the mouse // Zone for rotating the character with the mouse
mouse_detector: mouse_detector::State, mouse_detector: mouse_detector::State,
@ -264,16 +264,25 @@ enum Message {
} }
impl Controls { impl Controls {
fn new(fonts: Fonts, imgs: Imgs, selected: Option<CharacterId>, default_name: String) -> Self { fn new(
fonts: Fonts,
imgs: Imgs,
selected: Option<CharacterId>,
default_name: String,
server_info: &ServerInfo,
) -> Self {
let version = common::util::DISPLAY_VERSION_LONG.clone(); let version = common::util::DISPLAY_VERSION_LONG.clone();
let alpha = format!("Veloren {}", common::util::DISPLAY_VERSION.as_str()); 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 { Self {
fonts, fonts,
imgs, imgs,
version, version,
alpha, alpha,
server_mismatched_version,
tooltip_manager: TooltipManager::new(TOOLTIP_HOVER_DUR, TOOLTIP_FADE_DUR), tooltip_manager: TooltipManager::new(TOOLTIP_HOVER_DUR, TOOLTIP_FADE_DUR),
mouse_detector: Default::default(), mouse_detector: Default::default(),
mode: Mode::select(Some(InfoContent::LoadingCharacters)), mode: Mode::select(Some(InfoContent::LoadingCharacters)),
@ -1210,14 +1219,46 @@ impl Controls {
}, },
}; };
Container::new( // TODO: There is probably a better way to conditionally add in the warning box
Column::with_children(vec![top_text.into(), content]) // here
.spacing(3) if let Some(mismatched_version) = &self.server_mismatched_version {
.width(Length::Fill) let warning = iced::Text::<IcedRenderer>::new(format!(
.height(Length::Fill), "{}\n{}: {} {}: {}",
) i18n.get("char_selection.version_mismatch"),
.padding(3) i18n.get("main.login.server_version"),
.into() 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<Event>, characters: &[CharacterItem]) { fn update(&mut self, message: Message, events: &mut Vec<Event>, characters: &[CharacterItem]) {
@ -1451,6 +1492,7 @@ impl CharSelectionUi {
Imgs::load(&mut ui).expect("Failed to load images"), Imgs::load(&mut ui).expect("Failed to load images"),
selected_character, selected_character,
default_name, default_name,
client.server_info(),
); );
Self { Self {

View File

@ -1,7 +1,7 @@
use client::{ use client::{
addr::ConnectionArgs, addr::ConnectionArgs,
error::{Error as ClientError, NetworkConnectError, NetworkError}, error::{Error as ClientError, NetworkConnectError, NetworkError},
Client, Client, ServerInfo,
}; };
use crossbeam::channel::{unbounded, Receiver, Sender, TryRecvError}; use crossbeam::channel::{unbounded, Receiver, Sender, TryRecvError};
use std::{ use std::{
@ -17,7 +17,10 @@ use tracing::{trace, warn};
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
NoAddress, NoAddress,
ClientError(ClientError), ClientError {
error: ClientError,
mismatched_server_info: Option<ServerInfo>,
},
ClientCrashed, ClientCrashed,
} }
@ -103,16 +106,21 @@ impl ClientInit {
if cancel2.load(Ordering::Relaxed) { if cancel2.load(Ordering::Relaxed) {
break; break;
} }
let mut mismatched_server_info = None;
match Client::new( match Client::new(
connection_args.clone(), connection_args.clone(),
view_distance, view_distance,
Arc::clone(&runtime2), Arc::clone(&runtime2),
&mut mismatched_server_info,
) )
.await .await
{ {
Ok(mut client) => { Ok(mut client) => {
if let Err(e) = client.register(username, password, trust_fn).await { 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; break 'tries;
} }
let _ = tx.send(Msg::Done(Ok(client))); let _ = tx.send(Msg::Done(Ok(client)));
@ -126,7 +134,10 @@ impl ClientInit {
}, },
Err(e) => { Err(e) => {
trace!(?e, "Aborting server connection attempt"); trace!(?e, "Aborting server connection attempt");
last_err = Some(Error::ClientError(e)); last_err = Some(Error::ClientError {
error: e,
mismatched_server_info,
});
break 'tries; break 'tries;
}, },
} }

View File

@ -14,12 +14,15 @@ use crate::{
}; };
#[cfg(feature = "singleplayer")] #[cfg(feature = "singleplayer")]
use client::addr::ConnectionArgs; 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 client_init::{ClientConnArgs, ClientInit, Error as InitError, Msg as InitMsg};
use common::{assets::AssetExt, comp}; use common::{assets::AssetExt, comp};
use common_base::span; use common_base::span;
use scene::Scene; use scene::Scene;
use std::sync::Arc; use std::{fmt::Debug, sync::Arc};
use tokio::runtime; use tokio::runtime;
use tracing::error; use tracing::error;
use ui::{Event as MainMenuEvent, MainMenuUi}; use ui::{Event as MainMenuEvent, MainMenuUi};
@ -129,7 +132,10 @@ impl PlayState for MainMenuState {
InitError::NoAddress => { InitError::NoAddress => {
localized_strings.get("main.login.server_not_found").into() 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!( client::Error::SpecsErr(e) => format!(
"{}: {}", "{}: {}",
localized_strings.get("main.login.internal_error"), localized_strings.get("main.login.internal_error"),
@ -169,23 +175,25 @@ impl PlayState for MainMenuState {
}, },
client::Error::NetworkErr(NetworkError::ConnectFailed( client::Error::NetworkErr(NetworkError::ConnectFailed(
NetworkConnectError::Handshake(InitProtocolError::WrongVersion(_)), NetworkConnectError::Handshake(InitProtocolError::WrongVersion(_)),
)) => localized_strings )) => get_network_error_text(
.get("main.login.network_wrong_version") &localized_strings,
.into(), localized_strings.get("main.login.network_wrong_version"),
client::Error::NetworkErr(e) => format!( mismatched_server_info,
"{}: {:?}",
localized_strings.get("main.login.network_error"),
e
), ),
client::Error::ParticipantErr(e) => format!( client::Error::NetworkErr(e) => get_network_error_text(
"{}: {:?}", &localized_strings,
localized_strings.get("main.login.network_error"), e,
e mismatched_server_info,
), ),
client::Error::StreamErr(e) => format!( client::Error::ParticipantErr(e) => get_network_error_text(
"{}: {:?}", &localized_strings,
localized_strings.get("main.login.network_error"), e,
e mismatched_server_info,
),
client::Error::StreamErr(e) => get_network_error_text(
&localized_strings,
e,
mismatched_server_info,
), ),
client::Error::Other(e) => { client::Error::Other(e) => {
format!("{}: {}", localized_strings.get("common.error"), 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<ServerInfo>,
) -> 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( fn attempt_login(
settings: &mut Settings, settings: &mut Settings,
info_message: &mut Option<String>, info_message: &mut Option<String>,