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, Msg as InitMsg};
21use common::{comp, event::UpdateCharacterMetadata};
22use common_base::span;
23use common_net::msg::ClientType;
24#[cfg(feature = "plugins")]
25use common_state::plugin::PluginMgr;
26use i18n::{LocalizationGuard, LocalizationHandle, fluent_args};
27#[cfg(feature = "singleplayer")]
28use server::ServerInitStage;
29#[cfg(any(feature = "singleplayer", feature = "plugins"))]
30use specs::WorldExt;
31use std::{cell::RefCell, path::Path, rc::Rc, sync::Arc};
32use tokio::runtime;
33use tracing::error;
34use ui::{Event as MainMenuEvent, MainMenuUi};
35
36pub use ui::rand_bg_image_spec;
37
38#[derive(Debug)]
39pub enum DetailedInitializationStage {
40 #[cfg(feature = "singleplayer")]
41 Singleplayer,
42 #[cfg(feature = "singleplayer")]
43 SingleplayerServer(ServerInitStage),
44 StartingMultiplayer,
45 Client(ClientInitStage),
46 CreatingRenderPipeline(usize, usize),
47}
48
49enum InitState {
50 None,
51 Client(ClientInit),
53 Pipeline(Box<Client>, hud::PersistedHudState),
55}
56
57impl InitState {
58 fn client(&self) -> Option<&ClientInit> {
59 if let Self::Client(client_init) = &self {
60 Some(client_init)
61 } else {
62 None
63 }
64 }
65}
66
67pub struct MainMenuState {
68 main_menu_ui: MainMenuUi,
69 init: InitState,
70 scene: Scene,
71}
72
73impl MainMenuState {
74 pub fn new(global_state: &mut GlobalState) -> Self {
76 Self {
77 main_menu_ui: MainMenuUi::new(global_state),
78 init: InitState::None,
79 scene: Scene::new(global_state.window.renderer_mut()),
80 }
81 }
82}
83
84impl PlayState for MainMenuState {
85 fn enter(&mut self, global_state: &mut GlobalState, _: Direction) {
86 if global_state.settings.audio.output.is_enabled() && global_state.audio.music_enabled() {
88 global_state.audio.play_title_music();
89 }
90
91 #[cfg(feature = "singleplayer")]
93 {
94 global_state.singleplayer = SingleplayerState::None;
95 }
96
97 self.main_menu_ui
99 .update_language(global_state.i18n, &global_state.settings);
100 self.main_menu_ui
102 .set_scale_mode(global_state.settings.interface.ui_scale);
103
104 #[cfg(feature = "discord")]
105 global_state.discord.enter_main_menu();
106 }
107
108 fn tick(&mut self, global_state: &mut GlobalState, events: Vec<Event>) -> PlayStateResult {
109 span!(_guard, "tick", "<MainMenuState as PlayState>::tick");
110
111 let localized_strings = &global_state.i18n.read();
113
114 #[cfg(feature = "singleplayer")]
116 {
117 if let Some(singleplayer) = global_state.singleplayer.as_running() {
118 if let Ok(stage_update) = singleplayer.init_stage_receiver.try_recv() {
119 self.main_menu_ui.update_stage(
120 DetailedInitializationStage::SingleplayerServer(stage_update),
121 );
122 }
123
124 match singleplayer.receiver.try_recv() {
125 Ok(Ok(())) => {
126 attempt_login(
128 &mut global_state.info_message,
129 "singleplayer".to_owned(),
130 "".to_owned(),
131 ConnectionArgs::Mpsc(14004),
132 &mut self.init,
133 &global_state.tokio_runtime,
134 global_state.settings.language.send_to_server.then_some(
135 global_state.settings.language.selected_language.clone(),
136 ),
137 &global_state.i18n,
138 &global_state.config_dir,
139 global_state.args.client_type.0,
140 );
141 },
142 Ok(Err(e)) => {
143 error!(?e, "Could not start server");
144 global_state.singleplayer = SingleplayerState::None;
145 self.init = InitState::None;
146 self.main_menu_ui.cancel_connection();
147 let server_err = match e {
148 server::Error::NetworkErr(e) => localized_strings
149 .get_msg_ctx("main-servers-network_error", &i18n::fluent_args! {
150 "raw_error" => e.to_string()
151 })
152 .into_owned(),
153 server::Error::ParticipantErr(e) => localized_strings
154 .get_msg_ctx(
155 "main-servers-participant_error",
156 &i18n::fluent_args! {
157 "raw_error" => e.to_string()
158 },
159 )
160 .into_owned(),
161 server::Error::StreamErr(e) => localized_strings
162 .get_msg_ctx("main-servers-stream_error", &i18n::fluent_args! {
163 "raw_error" => e.to_string()
164 })
165 .into_owned(),
166 server::Error::DatabaseErr(e) => localized_strings
167 .get_msg_ctx("main-servers-database_error", &i18n::fluent_args! {
168 "raw_error" => e.to_string()
169 })
170 .into_owned(),
171 server::Error::PersistenceErr(e) => localized_strings
172 .get_msg_ctx(
173 "main-servers-persistence_error",
174 &i18n::fluent_args! {
175 "raw_error" => e.to_string()
176 },
177 )
178 .into_owned(),
179 server::Error::RtsimError(e) => localized_strings
180 .get_msg_ctx("main-servers-rtsim_error", &i18n::fluent_args! {
181 "raw_error" => e.to_string(),
182 })
183 .into_owned(),
184 server::Error::Other(e) => localized_strings
185 .get_msg_ctx("main-servers-other_error", &i18n::fluent_args! {
186 "raw_error" => e,
187 })
188 .into_owned(),
189 };
190 global_state.info_message = Some(
191 localized_strings
192 .get_msg_ctx(
193 "main-servers-singleplayer_error",
194 &i18n::fluent_args! {
195 "sp_error" => server_err
196 },
197 )
198 .into_owned(),
199 );
200 },
201 Err(_) => (),
202 }
203 }
204 }
205 for event in events {
207 if self.main_menu_ui.handle_event(event.clone()) {
209 continue;
210 }
211
212 match event {
213 Event::Close => return PlayStateResult::Shutdown,
214 _ => {},
216 }
217 }
218
219 if let Some(client_stage_update) = self.init.client().and_then(|init| init.stage_update()) {
220 self.main_menu_ui
221 .update_stage(DetailedInitializationStage::Client(client_stage_update));
222 }
223
224 match self.init.client().and_then(|init| init.poll()) {
226 Some(InitMsg::Done(Ok(mut client))) => {
227 #[cfg(feature = "plugins")]
229 for path in client.take_local_plugins().drain(..) {
230 if let Err(e) = client
231 .state_mut()
232 .ecs_mut()
233 .write_resource::<PluginMgr>()
234 .load_server_plugin(path)
235 {
236 tracing::error!(?e, "load local plugin");
237 }
238 }
239 crate::ecs::init(client.state_mut().ecs_mut());
241 self.init =
242 InitState::Pipeline(Box::new(client), hud::PersistedHudState::default());
243 },
244 Some(InitMsg::Done(Err(e))) => {
245 self.init = InitState::None;
246 error!(?e, "Client Init failed raw error");
247 let e = get_client_init_msg_error(e, &global_state.i18n);
248 error!(?e, "Client Init failed");
251 global_state.info_message = Some(
252 localized_strings
253 .get_msg_ctx("main-login-client_init_failed", &i18n::fluent_args! {
254 "init_fail_reason" => e
255 })
256 .into_owned(),
257 );
258 },
259 Some(InitMsg::IsAuthTrusted(auth_server)) => {
260 if global_state
261 .settings
262 .networking
263 .trusted_auth_servers
264 .contains(&auth_server)
265 {
266 self.init.client().unwrap().auth_trust(auth_server, true);
268 } else {
269 self.main_menu_ui.auth_trust_prompt(auth_server);
271 }
272 },
273 None => {},
274 }
275
276 if let InitState::Pipeline(client, _) = &mut self.init {
278 match client.tick(comp::ControllerInputs::default(), global_state.clock.dt()) {
279 Ok(events) => {
280 for event in events {
281 match event {
282 client::Event::SetViewDistance(_vd) => {},
283 client::Event::Disconnect => {
284 global_state.info_message = Some(
285 localized_strings
286 .get_msg("main-login-server_shut_down")
287 .into_owned(),
288 );
289 self.init = InitState::None;
290 },
291 client::Event::Chat(m) => {
292 if let InitState::Pipeline(client, persisted_state) = &mut self.init
293 {
294 persisted_state.message_backlog.new_message(
295 client,
296 &global_state.profile,
297 m,
298 )
299 }
300 },
301 client::Event::MapMarker(marker_event) => {
302 if let InitState::Pipeline(_client, persisted_state) =
303 &mut self.init
304 {
305 persisted_state.location_markers.update(marker_event);
306 }
307 },
308 #[cfg_attr(not(feature = "plugins"), expect(unused_variables))]
309 client::Event::PluginDataReceived(data) => {
310 #[cfg(feature = "plugins")]
311 {
312 tracing::info!("plugin data {}", data.len());
313 if let InitState::Pipeline(client, _) = &mut self.init {
314 let hash = client
315 .state()
316 .ecs()
317 .write_resource::<PluginMgr>()
318 .cache_server_plugin(&global_state.config_dir, data);
319 match hash {
320 Ok(hash) => {
321 if client.plugin_received(hash) == 0 {
322 client.load_character_list();
325 }
326 },
327 Err(e) => tracing::error!(?e, "cache_server_plugin"),
328 }
329 }
330 }
331 },
332 _ => {},
333 }
334 }
335 },
336 Err(err) => {
337 error!(?err, "[main menu] Failed to tick the client");
338 global_state.info_message =
339 Some(get_client_msg_error(err, None, &global_state.i18n.read()));
340 self.init = InitState::None;
341 },
342 }
343 }
344
345 if let InitState::Pipeline(..) = &self.init {
347 if let Some((done, total)) = &global_state.window.renderer().pipeline_creation_status()
348 {
349 self.main_menu_ui.update_stage(
350 DetailedInitializationStage::CreatingRenderPipeline(*done, *total),
351 );
352 } else {
354 if let InitState::Pipeline(mut client, persisted_state) =
356 core::mem::replace(&mut self.init, InitState::None)
357 {
358 self.main_menu_ui.connected();
359
360 if client.client_type().can_spectate()
363 && !client.client_type().can_enter_character()
364 {
365 client.request_spectate(global_state.settings.graphics.view_distances());
366
367 return PlayStateResult::Push(Box::new(SessionState::new(
368 global_state,
369 UpdateCharacterMetadata::default(),
370 Rc::new(RefCell::new(*client)),
371 Rc::new(RefCell::new(persisted_state)),
372 )));
373 }
374
375 let server_info = client.server_info().clone();
376 let server_description = client.server_description().clone();
377
378 let char_select = CharSelectionState::new(
379 global_state,
380 Rc::new(RefCell::new(*client)),
381 Rc::new(RefCell::new(persisted_state)),
382 );
383
384 let new_state = ServerInfoState::try_from_server_info(
385 global_state,
386 self.main_menu_ui.bg_img_spec(),
387 char_select,
388 server_info,
389 server_description,
390 false,
391 )
392 .map(|s| Box::new(s) as _)
393 .unwrap_or_else(|s| Box::new(s) as _);
394
395 return PlayStateResult::Push(new_state);
396 }
397 }
398 }
399
400 for event in self
402 .main_menu_ui
403 .maintain(global_state, global_state.clock.dt())
404 {
405 match event {
406 MainMenuEvent::LoginAttempt {
407 username,
408 password,
409 server_address,
410 } => {
411 let net_settings = &mut global_state.settings.networking;
412 let use_srv = net_settings.use_srv;
413 let use_quic = net_settings.use_quic;
414 let validate_tls = net_settings.validate_tls;
415 net_settings.username.clone_from(&username);
416 net_settings.default_server.clone_from(&server_address);
417 if !server_address.is_empty() && !net_settings.servers.contains(&server_address)
418 {
419 net_settings.servers.push(server_address.clone());
420 }
421 global_state
422 .settings
423 .save_to_file_warn(&global_state.config_dir);
424
425 let connection_args = if use_srv {
426 ConnectionArgs::Srv {
427 hostname: server_address,
428 prefer_ipv6: false,
429 validate_tls,
430 use_quic,
431 }
432 } else if use_quic {
433 ConnectionArgs::Quic {
434 hostname: server_address,
435 prefer_ipv6: false,
436 validate_tls,
437 }
438 } else {
439 ConnectionArgs::Tcp {
440 hostname: server_address,
441 prefer_ipv6: false,
442 }
443 };
444 attempt_login(
445 &mut global_state.info_message,
446 username,
447 password,
448 connection_args,
449 &mut self.init,
450 &global_state.tokio_runtime,
451 global_state
452 .settings
453 .language
454 .send_to_server
455 .then_some(global_state.settings.language.selected_language.clone()),
456 &global_state.i18n,
457 &global_state.config_dir,
458 global_state.args.client_type.0,
459 );
460 },
461 MainMenuEvent::CancelLoginAttempt => {
462 #[cfg(feature = "singleplayer")]
466 {
467 global_state.singleplayer = SingleplayerState::None;
468 }
469 self.init = InitState::None;
470 self.main_menu_ui.cancel_connection();
471 },
472 MainMenuEvent::ChangeLanguage(new_language) => {
473 global_state.settings.language.selected_language =
474 new_language.language_identifier;
475 global_state.i18n = LocalizationHandle::load_expect(
476 &global_state.settings.language.selected_language,
477 );
478 global_state
479 .i18n
480 .set_english_fallback(global_state.settings.language.use_english_fallback);
481 self.main_menu_ui
482 .update_language(global_state.i18n, &global_state.settings);
483 },
484 #[cfg(feature = "singleplayer")]
485 MainMenuEvent::StartSingleplayer => {
486 global_state.singleplayer.run(&global_state.tokio_runtime);
487 },
488 #[cfg(feature = "singleplayer")]
489 MainMenuEvent::InitSingleplayer => {
490 global_state.singleplayer = SingleplayerState::init();
491 },
492 #[cfg(feature = "singleplayer")]
493 MainMenuEvent::SinglePlayerChange(change) => {
494 if let SingleplayerState::Init(ref mut init) = global_state.singleplayer {
495 match change {
496 ui::WorldsChange::SetActive(world) => init.current = world,
497 ui::WorldsChange::Delete(world) => init.remove(world),
498 ui::WorldsChange::Regenerate(world) => init.delete_map_file(world),
499 ui::WorldsChange::AddNew => init.new_world(),
500 ui::WorldsChange::CurrentWorldChange(change) => {
501 if let Some(world) = init.current.map(|i| &mut init.worlds[i]) {
502 change.apply(world);
503 init.save_current_meta();
504 }
505 },
506 }
507 }
508 },
509 MainMenuEvent::Quit => return PlayStateResult::Shutdown,
510 MainMenuEvent::AuthServerTrust(auth_server, trust) => {
515 if trust {
516 global_state
517 .settings
518 .networking
519 .trusted_auth_servers
520 .insert(auth_server.clone());
521 global_state
522 .settings
523 .save_to_file_warn(&global_state.config_dir);
524 }
525 self.init
526 .client()
527 .map(|init| init.auth_trust(auth_server, trust));
528 },
529 MainMenuEvent::DeleteServer { server_index } => {
530 let net_settings = &mut global_state.settings.networking;
531 net_settings.servers.remove(server_index);
532
533 global_state
534 .settings
535 .save_to_file_warn(&global_state.config_dir);
536 },
537 }
538 }
539
540 if let Some(info) = global_state.info_message.take() {
541 self.main_menu_ui.show_info(info);
542 }
543
544 PlayStateResult::Continue
545 }
546
547 fn name(&self) -> &'static str { "Title" }
548
549 fn capped_fps(&self) -> bool { true }
550
551 fn globals_bind_group(&self) -> &GlobalsBindGroup { self.scene.global_bind_group() }
552
553 fn render(&self, drawer: &mut Drawer<'_>, _: &Settings) {
554 let mut third_pass = drawer.third_pass();
556 if let Some(mut ui_drawer) = third_pass.draw_ui() {
557 self.main_menu_ui.render(&mut ui_drawer);
558 };
559 }
560
561 fn egui_enabled(&self) -> bool { false }
562}
563
564pub(crate) fn get_client_msg_error(
565 error: client::Error,
566 mismatched_server_info: Option<ServerInfo>,
567 localization: &LocalizationGuard,
568) -> String {
569 let net_error = |error: String, mismatched_server_info: Option<ServerInfo>| -> String {
573 if let Some(server_info) =
574 mismatched_server_info.filter(|info| info.git_hash != *common::util::GIT_HASH)
575 {
576 format!(
577 "{} {}: {} ({}) {}: {} ({})",
578 localization.get_msg("main-login-network_wrong_version"),
579 localization.get_msg("main-login-client_version"),
580 &*common::util::GIT_HASH,
581 &*common::util::GIT_DATE,
582 localization.get_msg("main-login-server_version"),
583 server_info.git_hash,
584 server_info.git_date,
585 )
586 } else {
587 format!(
588 "{}: {}",
589 localization.get_msg("main-login-network_error"),
590 error
591 )
592 }
593 };
594
595 use client::Error;
596 match error {
597 Error::SpecsErr(e) => {
598 format!(
599 "{}: {}",
600 localization.get_msg("main-login-internal_error"),
601 e
602 )
603 },
604 Error::AuthErr(e) => format!(
605 "{}: {}",
606 localization.get_msg("main-login-authentication_error"),
607 e
608 ),
609 Error::Kicked(reason) => localization
610 .get_msg_ctx("main-login-kicked", &fluent_args! {
611 "reason" => reason,
612 })
613 .into(),
614 Error::TooManyPlayers => localization.get_msg("main-login-server_full").into(),
615 Error::AuthServerNotTrusted => localization
616 .get_msg("main-login-untrusted_auth_server")
617 .into(),
618 Error::ServerTimeout => localization.get_msg("main-login-timeout").into(),
619 Error::ServerShutdown => localization.get_msg("main-login-server_shut_down").into(),
620 Error::NotOnWhitelist => localization.get_msg("main-login-not_on_whitelist").into(),
621 Error::Banned(ban_info) => if let Some(end_time) = ban_info
622 .until
623 .and_then(|timestamp| DateTime::<Utc>::from_timestamp(timestamp, 0))
624 {
625 let end_date = end_time.with_timezone(&Local);
626 let end_date_str = end_date.format("%Y-%m-%d %H:%M").to_string();
627
628 localization.get_msg_ctx("main-login-banned_until", &fluent_args! {
629 "reason" => ban_info.reason,
630 "end_date" => end_date_str,
631 })
632 } else {
633 localization.get_msg_ctx("main-login-banned", &fluent_args! {
634 "reason" => ban_info.reason
635 })
636 }
637 .into(),
638 Error::InvalidCharacter => localization.get_msg("main-login-invalid_character").into(),
639 Error::NetworkErr(NetworkError::ConnectFailed(NetworkConnectError::Handshake(
640 InitProtocolError::WrongVersion(_),
641 ))) => net_error(
642 localization
643 .get_msg("main-login-network_wrong_version")
644 .into_owned(),
645 mismatched_server_info,
646 ),
647 Error::NetworkErr(e) => net_error(e.to_string(), mismatched_server_info),
648 Error::ParticipantErr(e) => net_error(e.to_string(), mismatched_server_info),
649 Error::StreamErr(e) => net_error(e.to_string(), mismatched_server_info),
650 Error::HostnameLookupFailed(e) => {
651 format!(
652 "{}: {}",
653 localization.get_msg("main-login-server_not_found"),
654 e
655 )
656 },
657 Error::Other(e) => {
658 format!("{}: {}", localization.get_msg("common-error"), e)
659 },
660 Error::AuthClientError(e) => match e {
661 client::AuthClientError::RequestError(e) => format!(
663 "{}: {}",
664 localization.get_msg("main-login-failed_sending_request"),
665 e
666 ),
667 client::AuthClientError::ResponseError(e) => format!(
668 "{}: {}",
669 localization.get_msg("main-login-failed_sending_request"),
670 e
671 ),
672 client::AuthClientError::CertificateLoad(e) => format!(
673 "{}: {}",
674 localization.get_msg("main-login-failed_sending_request"),
675 e
676 ),
677 client::AuthClientError::JsonError(e) => format!(
678 "{}: {}",
679 localization.get_msg("main-login-failed_sending_request"),
680 e
681 ),
682 client::AuthClientError::InsecureSchema => localization
683 .get_msg("main-login-insecure_auth_scheme")
684 .into(),
685 client::AuthClientError::ServerError(_, e) => String::from_utf8_lossy(&e).into(),
686 },
687 Error::AuthServerUrlInvalid(e) => {
688 format!(
689 "{}: https://{}",
690 localization.get_msg("main-login-failed_auth_server_url_invalid"),
691 e
692 )
693 },
694 }
695}
696
697fn get_client_init_msg_error(
698 error: client_init::Error,
699 localized_strings: &LocalizationHandle,
700) -> String {
701 let localization = localized_strings.read();
702
703 match error {
704 InitError::ClientError {
705 error,
706 mismatched_server_info,
707 } => get_client_msg_error(error, mismatched_server_info, &localization),
708 InitError::ClientCrashed => localization.get_msg("main-login-client_crashed").into(),
709 InitError::ServerNotFound => localization.get_msg("main-login-server_not_found").into(),
710 }
711}
712
713fn attempt_login(
714 info_message: &mut Option<String>,
715 username: String,
716 password: String,
717 connection_args: ConnectionArgs,
718 init: &mut InitState,
719 runtime: &Arc<runtime::Runtime>,
720 locale: Option<String>,
721 localized_strings: &LocalizationHandle,
722 config_dir: &Path,
723 client_type: ClientType,
724) {
725 let localization = localized_strings.read();
726 if let Err(err) = comp::Player::alias_validate(&username) {
727 match err {
728 comp::AliasError::ForbiddenCharacters => {
729 *info_message = Some(
730 localization
731 .get_msg("main-login-username_bad_characters")
732 .into_owned(),
733 );
734 },
735 comp::AliasError::TooLong => {
736 *info_message = Some(
737 localization
738 .get_msg_ctx("main-login-username_too_long", &i18n::fluent_args! {
739 "max_len" => comp::MAX_ALIAS_LEN
740 })
741 .into_owned(),
742 );
743 },
744 }
745 return;
746 }
747
748 if let InitState::None = init {
750 *init = InitState::Client(ClientInit::new(
751 connection_args,
752 username,
753 password,
754 Arc::clone(runtime),
755 locale,
756 config_dir,
757 client_type,
758 ));
759 }
760}