Skip to main content

veloren_voxygen/menu/main/
mod.rs

1pub(crate) mod client_init;
2mod ui;
3
4use super::{char_selection::CharSelectionState, dummy_scene::Scene, server_info::ServerInfoState};
5#[cfg(feature = "singleplayer")]
6use crate::singleplayer::SingleplayerState;
7use crate::{
8    Direction, GlobalState, PlayState, PlayStateResult, hud,
9    render::{Drawer, GlobalsBindGroup},
10    session::SessionState,
11    settings::Settings,
12    window::Event,
13};
14use chrono::{DateTime, Local, Utc};
15use client::{
16    Client, ClientInitStage, ServerInfo,
17    addr::ConnectionArgs,
18    error::{InitProtocolError, NetworkConnectError, NetworkError},
19};
20use client_init::{ClientInit, Error as InitError, InitOutMsg};
21use common::{comp, event::UpdateCharacterMetadata};
22use common_base::span;
23use common_net::msg::ClientType;
24#[cfg(feature = "plugins")]
25use common_state::plugin::PluginMgr;
26use credentialstore::*;
27use i18n::{LocalizationGuard, LocalizationHandle, fluent_args};
28#[cfg(feature = "singleplayer")]
29use server::ServerInitStage;
30#[cfg(any(feature = "singleplayer", feature = "plugins"))]
31use specs::WorldExt;
32use std::{cell::RefCell, path::Path, rc::Rc, sync::Arc};
33use tokio::{
34    runtime,
35    sync::mpsc,
36    time::{Duration, interval},
37};
38use tracing::error;
39use ui::{Event as MainMenuEvent, MainMenuUi};
40
41pub use ui::rand_bg_image_spec;
42
43pub enum CredentialResponse {
44    Status(CredentialStatus),
45    Password(Result<String, CredentialError>),
46}
47
48#[derive(Debug)]
49pub enum DetailedInitializationStage {
50    #[cfg(feature = "singleplayer")]
51    Singleplayer,
52    #[cfg(feature = "singleplayer")]
53    SingleplayerServer(ServerInitStage),
54    StartingMultiplayer,
55    Client(ClientInitStage),
56    CreatingRenderPipeline(usize, usize),
57}
58
59enum InitState {
60    None,
61    // Waiting on the client initialization
62    Client(ClientInit),
63    // Client initialized but still waiting on Renderer pipeline creation
64    Pipeline(Box<Client>, hud::PersistedHudState),
65}
66
67impl InitState {
68    fn client(&self) -> Option<&ClientInit> {
69        if let Self::Client(client_init) = &self {
70            Some(client_init)
71        } else {
72            None
73        }
74    }
75}
76
77pub struct MainMenuState {
78    main_menu_ui: MainMenuUi,
79    init: InitState,
80    scene: Scene,
81    secure_service: SecureService,
82    secure_service_rx: Option<mpsc::UnboundedReceiver<CredentialResponse>>,
83    secure_service_tx: Option<mpsc::Sender<String>>,
84    pending_credentials: Option<Credentials>,
85}
86
87impl MainMenuState {
88    /// Create a new `MainMenuState`.
89    pub fn new(global_state: &mut GlobalState) -> Self {
90        Self {
91            main_menu_ui: MainMenuUi::new(global_state),
92            init: InitState::None,
93            scene: Scene::new(global_state.window.renderer_mut()),
94            secure_service: SecureService::default(),
95            secure_service_rx: None,
96            secure_service_tx: None,
97            pending_credentials: None,
98        }
99    }
100}
101
102impl PlayState for MainMenuState {
103    fn enter(&mut self, global_state: &mut GlobalState, _: Direction) {
104        // Kick off title music
105        if global_state.settings.audio.output.is_enabled() && global_state.audio.music_enabled() {
106            global_state.audio.play_title_music();
107        }
108
109        // Reset singleplayer server if it was running already
110        #[cfg(feature = "singleplayer")]
111        {
112            global_state.singleplayer = SingleplayerState::None;
113        }
114
115        // Updated localization in case the selected language was changed
116        self.main_menu_ui
117            .update_language(global_state.i18n, &global_state.settings);
118        // Set scale mode in case it was change
119        self.main_menu_ui
120            .set_scale_mode(global_state.settings.interface.ui_scale);
121
122        #[cfg(feature = "discord")]
123        global_state.discord.enter_main_menu();
124    }
125
126    fn tick(&mut self, global_state: &mut GlobalState, events: Vec<Event>) -> PlayStateResult {
127        span!(_guard, "tick", "<MainMenuState as PlayState>::tick");
128
129        // Poll the credential store status to auto-fill password on unlock.
130        // Update UI on status changes.
131        if let Some(receiver) = &mut self.secure_service_rx
132            && let Some(transmitter) = &mut self.secure_service_tx
133        {
134            while let Ok(response) = receiver.try_recv() {
135                match response {
136                    CredentialResponse::Status(status) => {
137                        self.main_menu_ui.update_secure_service_status(status);
138                        if status == CredentialStatus::Ready
139                            && global_state.settings.interface.use_secure_service
140                        {
141                            let _ = transmitter.try_send(self.main_menu_ui.username());
142                        }
143                    },
144                    CredentialResponse::Password(Ok(password)) => {
145                        self.main_menu_ui.set_password(password)
146                    },
147                    CredentialResponse::Password(Err(error)) => {
148                        tracing::warn!(?error, "{error}")
149                    },
150                }
151            }
152        }
153
154        // Pull in localizations
155        let localized_strings = &global_state.i18n.read();
156
157        // Poll server creation
158        #[cfg(feature = "singleplayer")]
159        {
160            if let Some(singleplayer) = global_state.singleplayer.as_running() {
161                if let Ok(stage_update) = singleplayer.init_stage_receiver.try_recv() {
162                    self.main_menu_ui.update_stage(
163                        DetailedInitializationStage::SingleplayerServer(stage_update),
164                    );
165                }
166
167                match singleplayer.receiver.try_recv() {
168                    Ok(Ok(())) => {
169                        // Attempt login after the server is finished initializing
170                        attempt_login(
171                            ConnectionArgs::Mpsc(14004),
172                            &mut self.init,
173                            &global_state.tokio_runtime,
174                            global_state.settings.language.send_to_server.then_some(
175                                global_state.settings.language.selected_language.clone(),
176                            ),
177                            &global_state.config_dir,
178                            global_state.args.client_type.0,
179                        );
180                    },
181                    Ok(Err(e)) => {
182                        error!(?e, "Could not start server");
183                        global_state.singleplayer = SingleplayerState::None;
184                        self.init = InitState::None;
185                        self.main_menu_ui.cancel_connection();
186                        let server_err = match e {
187                            server::Error::NetworkErr(e) => localized_strings
188                                .get_msg_ctx("main-servers-network_error", &i18n::fluent_args! {
189                                    "raw_error" => e.to_string()
190                                })
191                                .into_owned(),
192                            server::Error::ParticipantErr(e) => localized_strings
193                                .get_msg_ctx(
194                                    "main-servers-participant_error",
195                                    &i18n::fluent_args! {
196                                        "raw_error" => e.to_string()
197                                    },
198                                )
199                                .into_owned(),
200                            server::Error::StreamErr(e) => localized_strings
201                                .get_msg_ctx("main-servers-stream_error", &i18n::fluent_args! {
202                                    "raw_error" => e.to_string()
203                                })
204                                .into_owned(),
205                            server::Error::DatabaseErr(e) => localized_strings
206                                .get_msg_ctx("main-servers-database_error", &i18n::fluent_args! {
207                                    "raw_error" => e.to_string()
208                                })
209                                .into_owned(),
210                            server::Error::PersistenceErr(e) => localized_strings
211                                .get_msg_ctx(
212                                    "main-servers-persistence_error",
213                                    &i18n::fluent_args! {
214                                        "raw_error" => e.to_string()
215                                    },
216                                )
217                                .into_owned(),
218                            server::Error::RtsimError(e) => localized_strings
219                                .get_msg_ctx("main-servers-rtsim_error", &i18n::fluent_args! {
220                                    "raw_error" => e.to_string(),
221                                })
222                                .into_owned(),
223                            server::Error::Other(e) => localized_strings
224                                .get_msg_ctx("main-servers-other_error", &i18n::fluent_args! {
225                                    "raw_error" => e,
226                                })
227                                .into_owned(),
228                        };
229                        global_state.info_message = Some(
230                            localized_strings
231                                .get_msg_ctx(
232                                    "main-servers-singleplayer_error",
233                                    &i18n::fluent_args! {
234                                        "sp_error" => server_err
235                                    },
236                                )
237                                .into_owned(),
238                        );
239                    },
240                    Err(_) => (),
241                }
242            }
243        }
244        // Handle window events.
245        for event in events {
246            // Pass all events to the ui first.
247            if self.main_menu_ui.handle_event(event.clone()) {
248                continue;
249            }
250
251            // Shutdown on Close, ignore all other events.
252            if matches!(event, Event::Close) {
253                return PlayStateResult::Shutdown;
254            }
255        }
256
257        if let Some(client_stage_update) = self.init.client().and_then(|init| init.stage_update()) {
258            self.main_menu_ui
259                .update_stage(DetailedInitializationStage::Client(client_stage_update));
260        }
261
262        // Poll client creation.
263        if let Some(clientinit) = self.init.client() {
264            match clientinit.poll() {
265                Some(InitOutMsg::Done(Ok(mut client))) => {
266                    // Stop secure service tasks and save pending credentials.
267                    self.secure_service_rx = None;
268                    self.secure_service_tx = None;
269                    if let Some(credentials) = self.pending_credentials.take() {
270                        let service = self.secure_service.clone();
271                        global_state.tokio_runtime.spawn(async move {
272                            let _ = service
273                                .save(credentials)
274                                .await
275                                .map_err(|error| tracing::warn!(?error, "{error}"));
276                        });
277                    }
278                    // load local plugins needed by the server
279                    #[cfg(feature = "plugins")]
280                    for path in client.take_local_plugins().drain(..) {
281                        if let Err(e) = client
282                            .state_mut()
283                            .ecs_mut()
284                            .write_resource::<PluginMgr>()
285                            .load_server_plugin(path)
286                        {
287                            tracing::error!(?e, "load local plugin");
288                        }
289                    }
290                    // Register voxygen components / resources
291                    crate::ecs::init(client.state_mut().ecs_mut());
292                    self.init =
293                        InitState::Pipeline(Box::new(client), hud::PersistedHudState::default());
294                },
295                Some(InitOutMsg::Done(Err(e))) => {
296                    self.init = InitState::None;
297                    error!(?e, "Client Init failed raw error");
298                    let e = get_client_init_msg_error(e, &global_state.i18n);
299                    // Stop secure service tasks.
300                    self.secure_service_rx = None;
301                    self.secure_service_tx = None;
302                    // Log error for possible additional use later or in case that the error
303                    // displayed is cut of.
304                    error!(?e, "Client Init failed");
305                    global_state.info_message = Some(
306                        localized_strings
307                            .get_msg_ctx("main-login-client_init_failed", &i18n::fluent_args! {
308                                "init_fail_reason" => e
309                            })
310                            .into_owned(),
311                    );
312                },
313                Some(InitOutMsg::IsAuthTrusted(auth_server)) => {
314                    if global_state
315                        .settings
316                        .networking
317                        .trusted_auth_servers
318                        .contains(&auth_server)
319                    {
320                        clientinit.auth_trust(auth_server, true);
321                    } else {
322                        // Show warning that auth server is not trusted and prompt for approval
323                        self.main_menu_ui.auth_trust_prompt(auth_server);
324                    }
325                },
326                Some(InitOutMsg::RequestCredentials(auth_server)) => {
327                    // Reset secure service state and start polling.
328                    if self.secure_service.is_supported()
329                        && let Some(auth_server) = auth_server.clone()
330                    {
331                        let service = self.secure_service.clone();
332                        let (command_tx, mut command_rx) = mpsc::channel::<String>(1);
333                        let (updates_tx, updates_rx) =
334                            mpsc::unbounded_channel::<CredentialResponse>();
335                        self.secure_service_rx = Some(updates_rx);
336                        self.secure_service_tx = Some(command_tx);
337                        global_state.tokio_runtime.spawn(async move {
338                            let mut interval = interval(Duration::from_secs(2));
339                            let mut request = String::new();
340                            let mut status = CredentialStatus::Disabled;
341                            while !updates_tx.is_closed() {
342                                tokio::select! {
343                                    _ = interval.tick() => {
344                                        let new_status = service.status().await;
345                                        if new_status != status {
346                                            status = new_status;
347                                            let _ = updates_tx.send(CredentialResponse::Status(status));
348                                        }
349                                    },
350                                    command = command_rx.recv() => {
351                                        match command {
352                                            Some(username) => {
353                                                if request != username {
354                                                    let password = service.load(auth_server.clone(), username.clone()).await;
355                                                    if password.is_ok() {
356                                                        request = username;
357                                                    }
358                                                    let _ = updates_tx.send(CredentialResponse::Password(password));
359                                                }
360                                            },
361                                            None => break,
362                                        }
363                                    }
364                                }
365                            }
366                        });
367                    }
368
369                    self.main_menu_ui.credentials_prompt(auth_server);
370                },
371                None => {},
372            }
373        }
374
375        // Tick the client to keep the connection alive if we are waiting on pipelines
376        if let InitState::Pipeline(client, _) = &mut self.init {
377            match client.tick(
378                comp::ControllerInputs::default(),
379                global_state.clock.game_dt(),
380            ) {
381                Ok(events) => {
382                    for event in events {
383                        match event {
384                            client::Event::SetViewDistance(_vd) => {},
385                            client::Event::Disconnect => {
386                                global_state.info_message = Some(
387                                    localized_strings
388                                        .get_msg("main-login-server_shut_down")
389                                        .into_owned(),
390                                );
391                                self.init = InitState::None;
392                            },
393                            client::Event::Chat(m) => {
394                                if let InitState::Pipeline(client, persisted_state) = &mut self.init
395                                {
396                                    persisted_state.message_backlog.new_message(
397                                        client,
398                                        &global_state.profile,
399                                        m,
400                                    )
401                                }
402                            },
403                            client::Event::MapMarker(marker_event) => {
404                                if let InitState::Pipeline(_client, persisted_state) =
405                                    &mut self.init
406                                {
407                                    persisted_state.location_markers.update(marker_event);
408                                }
409                            },
410                            #[cfg_attr(not(feature = "plugins"), expect(unused_variables))]
411                            client::Event::PluginDataReceived(data) => {
412                                #[cfg(feature = "plugins")]
413                                {
414                                    tracing::info!("plugin data {}", data.len());
415                                    if let InitState::Pipeline(client, _) = &mut self.init {
416                                        let hash = client
417                                            .state()
418                                            .ecs()
419                                            .write_resource::<PluginMgr>()
420                                            .cache_server_plugin(&global_state.config_dir, data);
421                                        match hash {
422                                            Ok(hash) => {
423                                                if client.plugin_received(hash) == 0 {
424                                                    // now load characters (plugins might contain
425                                                    // items)
426                                                    client.load_character_list();
427                                                }
428                                            },
429                                            Err(e) => tracing::error!(?e, "cache_server_plugin"),
430                                        }
431                                    }
432                                }
433                            },
434                            _ => {},
435                        }
436                    }
437                },
438                Err(err) => {
439                    error!(?err, "[main menu] Failed to tick the client");
440                    global_state.info_message =
441                        Some(get_client_msg_error(err, None, &global_state.i18n.read()));
442                    self.init = InitState::None;
443                },
444            }
445        }
446
447        // Poll renderer pipeline creation
448        if let InitState::Pipeline(..) = &self.init {
449            if let Some((done, total)) = &global_state.window.renderer().pipeline_creation_status()
450            {
451                self.main_menu_ui.update_stage(
452                    DetailedInitializationStage::CreatingRenderPipeline(*done, *total),
453                );
454            // If complete go to char select screen
455            } else {
456                // Always succeeds since we check above
457                if let InitState::Pipeline(mut client, persisted_state) =
458                    core::mem::replace(&mut self.init, InitState::None)
459                {
460                    self.main_menu_ui.connected();
461
462                    // If the client cannot enter the game but spectate, skip from the character
463                    // menu directly to spectating.
464                    if client.client_type().can_spectate()
465                        && !client.client_type().can_enter_character()
466                    {
467                        client.request_spectate(global_state.settings.graphics.view_distances());
468
469                        return PlayStateResult::Push(Box::new(SessionState::new(
470                            global_state,
471                            UpdateCharacterMetadata::default(),
472                            Rc::new(RefCell::new(*client)),
473                            Rc::new(RefCell::new(persisted_state)),
474                        )));
475                    }
476
477                    let server_info = client.server_info().clone();
478                    let server_description = client.server_description().clone();
479
480                    let char_select = CharSelectionState::new(
481                        global_state,
482                        Rc::new(RefCell::new(*client)),
483                        Rc::new(RefCell::new(persisted_state)),
484                    );
485
486                    let new_state = ServerInfoState::try_from_server_info(
487                        global_state,
488                        self.main_menu_ui.bg_img_spec(),
489                        char_select,
490                        server_info,
491                        server_description,
492                        false,
493                    )
494                    .map(|s| Box::new(s) as _)
495                    .unwrap_or_else(|s| Box::new(s) as _);
496
497                    return PlayStateResult::Push(new_state);
498                }
499            }
500        }
501
502        // Maintain the UI.
503        for event in self
504            .main_menu_ui
505            .maintain(global_state, global_state.clock.real_dt())
506        {
507            match event {
508                MainMenuEvent::LoginAttempt { server_address } => {
509                    let net_settings = &mut global_state.settings.networking;
510                    let use_srv = net_settings.use_srv;
511                    let use_quic = net_settings.use_quic;
512                    let validate_tls = net_settings.validate_tls;
513                    net_settings.default_server.clone_from(&server_address);
514                    if !server_address.is_empty() && !net_settings.servers.contains(&server_address)
515                    {
516                        net_settings.servers.push(server_address.clone());
517                    }
518                    global_state
519                        .settings
520                        .save_to_file_warn(&global_state.config_dir);
521
522                    let connection_args = if use_srv {
523                        ConnectionArgs::Srv {
524                            hostname: server_address,
525                            prefer_ipv6: false,
526                            validate_tls,
527                            use_quic,
528                        }
529                    } else if use_quic {
530                        ConnectionArgs::Quic {
531                            hostname: server_address,
532                            prefer_ipv6: false,
533                            validate_tls,
534                        }
535                    } else {
536                        ConnectionArgs::Tcp {
537                            hostname: server_address,
538                            prefer_ipv6: false,
539                        }
540                    };
541                    attempt_login(
542                        connection_args,
543                        &mut self.init,
544                        &global_state.tokio_runtime,
545                        global_state
546                            .settings
547                            .language
548                            .send_to_server
549                            .then_some(global_state.settings.language.selected_language.clone()),
550                        &global_state.config_dir,
551                        global_state.args.client_type.0,
552                    );
553                },
554                MainMenuEvent::CancelLoginAttempt => {
555                    // Stop secure service tasks.
556                    self.secure_service_rx = None;
557                    self.secure_service_tx = None;
558                    // init contains InitState::Client(ClientInit), which spawns a thread which
559                    // contains a TcpStream::connect() call This call is
560                    // blocking TODO fix when the network rework happens
561                    #[cfg(feature = "singleplayer")]
562                    {
563                        global_state.singleplayer = SingleplayerState::None;
564                    }
565                    self.init = InitState::None;
566                    self.main_menu_ui.cancel_connection();
567                },
568                MainMenuEvent::ChangeLanguage(new_language) => {
569                    global_state.settings.language.selected_language =
570                        new_language.language_identifier;
571                    global_state.i18n = LocalizationHandle::load_expect(
572                        &global_state.settings.language.selected_language,
573                    );
574                    global_state
575                        .i18n
576                        .set_english_fallback(global_state.settings.language.use_english_fallback);
577                    self.main_menu_ui
578                        .update_language(global_state.i18n, &global_state.settings);
579                },
580                #[cfg(feature = "singleplayer")]
581                MainMenuEvent::StartSingleplayer => {
582                    global_state.singleplayer.run(
583                        &global_state.tokio_runtime,
584                        &global_state.settings.language.selected_language,
585                        &global_state.i18n,
586                    );
587                },
588                #[cfg(feature = "singleplayer")]
589                MainMenuEvent::InitSingleplayer => {
590                    global_state.singleplayer = SingleplayerState::init();
591                },
592                #[cfg(feature = "singleplayer")]
593                MainMenuEvent::SinglePlayerChange(change) => {
594                    if let SingleplayerState::Init(ref mut init) = global_state.singleplayer {
595                        match change {
596                            ui::WorldsChange::SetActive(world) => init.current = world,
597                            ui::WorldsChange::Delete(world) => init.remove(world),
598                            ui::WorldsChange::Regenerate(world) => init.delete_map_file(world),
599                            ui::WorldsChange::AddNew => init.new_world(),
600                            ui::WorldsChange::CurrentWorldChange(change) => {
601                                if let Some(world) = init.current.map(|i| &mut init.worlds[i]) {
602                                    change.apply(world);
603                                    init.save_current_meta();
604                                }
605                            },
606                        }
607                    }
608                },
609                MainMenuEvent::Quit => return PlayStateResult::Shutdown,
610                // Note: Keeping in case we re-add the disclaimer
611                /*MainMenuEvent::DisclaimerAccepted => {
612                    global_state.settings.show_disclaimer = false
613                },*/
614                MainMenuEvent::AuthServerTrust(auth_server, trust) => {
615                    if trust {
616                        global_state
617                            .settings
618                            .networking
619                            .trusted_auth_servers
620                            .insert(auth_server.clone());
621                        global_state
622                            .settings
623                            .save_to_file_warn(&global_state.config_dir);
624                    }
625                    self.init
626                        .client()
627                        .map(|init| init.auth_trust(auth_server, trust));
628                },
629                MainMenuEvent::CredentialsProvided(auth_server, credentials) => {
630                    // Once we provided Credentials, store the Server settings and username.
631                    // Keep pending credentials to save them on successful login.
632                    let net_settings = &mut global_state.settings.networking;
633
634                    self.pending_credentials = if global_state.settings.interface.use_secure_service
635                        && let Some(auth_server) = auth_server.as_ref()
636                        && let Ok(client::Credentials::UsernamePassword(username, password)) =
637                            credentials.as_ref()
638                    {
639                        Some(Credentials {
640                            auth_server: auth_server.clone(),
641                            username: username.clone(),
642                            password: password.clone(),
643                        })
644                    } else {
645                        None
646                    };
647
648                    let _ = credentials.as_ref().map(|credentials| {
649                        let username = match credentials {
650                            client::Credentials::Username(username) => username,
651                            client::Credentials::UsernamePassword(username, _) => username,
652                        };
653                        net_settings.username.clone_from(username);
654                    });
655                    global_state
656                        .settings
657                        .save_to_file_warn(&global_state.config_dir);
658
659                    self.init
660                        .client()
661                        .map(|init| init.provide_credentials(auth_server, credentials));
662                },
663                MainMenuEvent::DeleteServer { server_index } => {
664                    let net_settings = &mut global_state.settings.networking;
665                    net_settings.servers.remove(server_index);
666
667                    global_state
668                        .settings
669                        .save_to_file_warn(&global_state.config_dir);
670                },
671                MainMenuEvent::UseSecureService(enabled) => {
672                    global_state.settings.interface.use_secure_service = enabled;
673                    global_state
674                        .settings
675                        .save_to_file_warn(&global_state.config_dir);
676                    // Load credentials immediately when "Remember password" is enabled. Calls with
677                    // the same values will be rejected inside the task and the queue is limited.
678                    if enabled
679                        && let Some(transmitter) = &mut self.secure_service_tx
680                        && let Err(error) = transmitter.try_send(self.main_menu_ui.username())
681                    {
682                        tracing::warn!(?error, "Credential store is overloaded")
683                    }
684                },
685            }
686        }
687
688        if let Some(info) = global_state.info_message.take() {
689            self.main_menu_ui.show_info(info);
690        }
691
692        PlayStateResult::Continue
693    }
694
695    fn name(&self) -> &'static str { "Title" }
696
697    fn capped_fps(&self) -> bool { true }
698
699    fn globals_bind_group(&self) -> &GlobalsBindGroup { self.scene.global_bind_group() }
700
701    fn render(&self, drawer: &mut Drawer<'_>, _: &Settings) {
702        // Draw the UI to the screen.
703        let mut third_pass = drawer.third_pass();
704        if let Some(mut ui_drawer) = third_pass.draw_ui() {
705            self.main_menu_ui.render(&mut ui_drawer);
706        };
707    }
708
709    fn egui_enabled(&self) -> bool { false }
710}
711
712pub(crate) fn get_client_msg_error(
713    error: client::Error,
714    mismatched_server_info: Option<ServerInfo>,
715    localization: &LocalizationGuard,
716) -> String {
717    // When a network error is received and there is a mismatch between the client
718    // and server version it is almost definitely due to this mismatch rather than
719    // a true networking error.
720    let net_error = |error: String, mismatched_server_info: Option<ServerInfo>| -> String {
721        if let Some(server_info) = mismatched_server_info.filter(|info| {
722            info.git_hash != *common::util::GIT_HASH
723                || info.git_timestamp != *common::util::GIT_TIMESTAMP
724        }) {
725            format!(
726                "{} {}: {} {}: {}",
727                localization.get_msg("main-login-network_wrong_version"),
728                localization.get_msg("main-login-client_version"),
729                *common::util::DISPLAY_VERSION,
730                localization.get_msg("main-login-server_version"),
731                common::util::make_display_version(server_info.git_hash, server_info.git_timestamp),
732            )
733        } else {
734            format!(
735                "{}: {}",
736                localization.get_msg("main-login-network_error"),
737                error
738            )
739        }
740    };
741
742    use client::Error;
743    match error {
744        Error::SpecsErr(e) => {
745            format!(
746                "{}: {}",
747                localization.get_msg("main-login-internal_error"),
748                e
749            )
750        },
751        Error::AuthErr(e) => format!(
752            "{}: {}",
753            localization.get_msg("main-login-authentication_error"),
754            e
755        ),
756        Error::Kicked(reason) => localization
757            .get_msg_ctx("main-login-kicked", &fluent_args! {
758                "reason" => reason,
759            })
760            .into(),
761        Error::TooManyPlayers => localization.get_msg("main-login-server_full").into(),
762        Error::AuthServerNotTrusted => localization
763            .get_msg("main-login-untrusted_auth_server")
764            .into(),
765        Error::CredentialsNotProvided => localization
766            .get_msg("main-login-credentials_not_provided")
767            .into(),
768        Error::ServerTimeout => localization.get_msg("main-login-timeout").into(),
769        Error::ServerShutdown => localization.get_msg("main-login-server_shut_down").into(),
770        Error::NotOnWhitelist => localization.get_msg("main-login-not_on_whitelist").into(),
771        Error::Banned(ban_info) => if let Some(end_time) = ban_info
772            .until
773            .and_then(|timestamp| DateTime::<Utc>::from_timestamp(timestamp, 0))
774        {
775            let end_date = end_time.with_timezone(&Local);
776            let end_date_str = end_date.format("%Y-%m-%d %H:%M").to_string();
777
778            localization.get_msg_ctx("main-login-banned_until", &fluent_args! {
779                "reason" => ban_info.reason,
780                "end_date" => end_date_str,
781            })
782        } else {
783            localization.get_msg_ctx("main-login-banned", &fluent_args! {
784                "reason" => ban_info.reason
785            })
786        }
787        .into(),
788        Error::InvalidCharacter => localization.get_msg("main-login-invalid_character").into(),
789        Error::NetworkErr(NetworkError::ConnectFailed(NetworkConnectError::Handshake(
790            InitProtocolError::WrongVersion(_),
791        ))) => net_error(
792            localization
793                .get_msg("main-login-network_wrong_version")
794                .into_owned(),
795            mismatched_server_info,
796        ),
797        Error::NetworkErr(e) => net_error(e.to_string(), mismatched_server_info),
798        Error::ParticipantErr(e) => net_error(e.to_string(), mismatched_server_info),
799        Error::StreamErr(e) => net_error(e.to_string(), mismatched_server_info),
800        Error::RustlsErr(e) => net_error(e.to_string(), mismatched_server_info),
801        Error::HostnameLookupFailed(e) => {
802            format!(
803                "{}: {}",
804                localization.get_msg("main-login-server_not_found"),
805                e
806            )
807        },
808        Error::Other(e) => {
809            format!("{}: {}", localization.get_msg("common-error"), e)
810        },
811        Error::AuthClientError(e) => match e {
812            // TODO: remove parentheses
813            client::AuthClientError::RequestError(e) => format!(
814                "{}: {}",
815                localization.get_msg("main-login-failed_sending_request"),
816                e
817            ),
818            client::AuthClientError::ResponseError(e) => format!(
819                "{}: {}",
820                localization.get_msg("main-login-failed_sending_request"),
821                e
822            ),
823            client::AuthClientError::CertificateLoad(e) => format!(
824                "{}: {}",
825                localization.get_msg("main-login-failed_sending_request"),
826                e
827            ),
828            client::AuthClientError::JsonError(e) => format!(
829                "{}: {}",
830                localization.get_msg("main-login-failed_sending_request"),
831                e
832            ),
833            client::AuthClientError::InsecureSchema => localization
834                .get_msg("main-login-insecure_auth_scheme")
835                .into(),
836            client::AuthClientError::ServerError(_, e) => String::from_utf8_lossy(&e).into(),
837        },
838        Error::AuthServerUrlInvalid(e) => {
839            format!(
840                "{}: https://{}",
841                localization.get_msg("main-login-failed_auth_server_url_invalid"),
842                e
843            )
844        },
845    }
846}
847
848fn get_client_init_msg_error(
849    error: client_init::Error,
850    localized_strings: &LocalizationHandle,
851) -> String {
852    let localization = localized_strings.read();
853
854    match error {
855        InitError::ClientError {
856            error,
857            mismatched_server_info,
858        } => get_client_msg_error(error, mismatched_server_info, &localization),
859        InitError::ClientCrashed => localization.get_msg("main-login-client_crashed").into(),
860        InitError::ServerNotFound => localization.get_msg("main-login-server_not_found").into(),
861    }
862}
863
864fn attempt_login(
865    connection_args: ConnectionArgs,
866    init: &mut InitState,
867    runtime: &Arc<runtime::Runtime>,
868    locale: Option<String>,
869    config_dir: &Path,
870    client_type: ClientType,
871) {
872    // Don't try to connect if there is already a connection in progress.
873    if let InitState::None = init {
874        *init = InitState::Client(ClientInit::new(
875            connection_args,
876            Arc::clone(runtime),
877            locale,
878            config_dir,
879            client_type,
880        ));
881    }
882}