From d6371f7f9bdb660bf08ec6487b3b1c7c49bd22a7 Mon Sep 17 00:00:00 2001 From: Maxicarlos08 Date: Sun, 14 Jan 2024 23:38:02 +0100 Subject: [PATCH] Server rules i18n and rules button in character screen --- assets/voxygen/i18n/en/char_selection.ftl | 1 + assets/voxygen/i18n/en/hud/settings.ftl | 1 + client/src/lib.rs | 16 +++- common/net/src/msg/client.rs | 1 + common/net/src/msg/server.rs | 7 +- common/src/cmd.rs | 6 +- server/src/client.rs | 3 + server/src/cmd.rs | 45 ++++++++-- server/src/connection_handler.rs | 1 + server/src/lib.rs | 4 +- server/src/settings.rs | 15 ++-- server/src/settings/server_description.rs | 80 +++++++++++++---- server/src/sys/msg/character_screen.rs | 7 +- server/src/sys/msg/register.rs | 26 +++++- voxygen/src/hud/settings_window/language.rs | 95 +++++++++++++-------- voxygen/src/menu/char_selection/mod.rs | 23 +++++ voxygen/src/menu/char_selection/ui/mod.rs | 32 ++++++- voxygen/src/menu/main/client_init.rs | 2 + voxygen/src/menu/main/mod.rs | 15 ++++ voxygen/src/menu/main/ui/mod.rs | 4 +- voxygen/src/menu/server_info/mod.rs | 48 +++++++---- voxygen/src/session/settings_change.rs | 4 + voxygen/src/settings/language.rs | 5 ++ 23 files changed, 345 insertions(+), 96 deletions(-) diff --git a/assets/voxygen/i18n/en/char_selection.ftl b/assets/voxygen/i18n/en/char_selection.ftl index 5d4bb54147..efa68847cd 100644 --- a/assets/voxygen/i18n/en/char_selection.ftl +++ b/assets/voxygen/i18n/en/char_selection.ftl @@ -26,3 +26,4 @@ char_selection-starting_site_name = { $name } char_selection-starting_site_kind = Kind: { $kind } 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. +char_selection-rules = Rules diff --git a/assets/voxygen/i18n/en/hud/settings.ftl b/assets/voxygen/i18n/en/hud/settings.ftl index fc2ec20847..c3c295b1ee 100644 --- a/assets/voxygen/i18n/en/hud/settings.ftl +++ b/assets/voxygen/i18n/en/hud/settings.ftl @@ -137,6 +137,7 @@ hud-settings-music_spacing = Music Spacing hud-settings-audio_device = Audio Device hud-settings-reset_sound = Reset to Defaults hud-settings-english_fallback = Display English for missing translations +hud-settings-language_share_with_server = Share configured language with servers (for localizing rules and motd) hud-settings-awaitingkey = Press a key... hud-settings-unbound = None hud-settings-reset_keybinds = Reset to Defaults diff --git a/client/src/lib.rs b/client/src/lib.rs index 6977133a8f..a9a241c4bd 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -56,6 +56,7 @@ use common_base::{prof_span, span}; use common_net::{ msg::{ self, + server::ServerDescription, world_msg::{EconomyInfo, PoiInfo, SiteId, SiteInfo}, ChatTypeContext, ClientGeneral, ClientMsg, ClientRegister, ClientType, DisconnectReason, InviteAnswer, Notification, PingMsg, PlayerInfo, PlayerListUpdate, RegisterError, @@ -228,6 +229,8 @@ pub struct Client { presence: Option, runtime: Arc, server_info: ServerInfo, + /// Localized server motd and rules + server_description: ServerDescription, world_data: WorldData, weather: WeatherLerp, player_list: HashMap, @@ -305,6 +308,7 @@ impl Client { mismatched_server_info: &mut Option, username: &str, password: &str, + locale: Option, auth_trusted: impl FnMut(&str) -> bool, init_stage_update: &(dyn Fn(ClientInitStage) + Send + Sync), add_foreign_systems: impl Fn(&mut DispatcherBuilder) + Send + 'static, @@ -365,6 +369,7 @@ impl Client { Self::register( username, password, + locale, auth_trusted, &server_info, &mut register_stream, @@ -386,6 +391,7 @@ impl Client { ability_map, server_constants, repair_recipe_book, + description, } = loop { tokio::select! { // Spawn in a blocking thread (leaving the network thread free). This is mostly @@ -725,6 +731,7 @@ impl Client { presence: None, runtime, server_info, + server_description: description, world_data: WorldData { lod_base, lod_alt, @@ -801,6 +808,7 @@ impl Client { async fn register( username: &str, password: &str, + locale: Option, mut auth_trusted: impl FnMut(&str) -> bool, server_info: &ServerInfo, register_stream: &mut Stream, @@ -838,7 +846,10 @@ impl Client { debug!("Registering client..."); - register_stream.send(ClientRegister { token_or_username })?; + register_stream.send(ClientRegister { + token_or_username, + locale, + })?; match register_stream.recv::().await? { Err(RegisterError::AuthError(err)) => Err(Error::AuthErr(err)), @@ -1182,6 +1193,8 @@ impl Client { pub fn server_info(&self) -> &ServerInfo { &self.server_info } + pub fn server_description(&self) -> &ServerDescription { &self.server_description } + pub fn world_data(&self) -> &WorldData { &self.world_data } pub fn recipe_book(&self) -> &RecipeBook { &self.recipe_book } @@ -3010,6 +3023,7 @@ mod tests { &mut None, username, password, + None, |suggestion: &str| suggestion == auth_server, &|_| {}, |_| {}, diff --git a/common/net/src/msg/client.rs b/common/net/src/msg/client.rs index f752db223d..8b70fbde15 100644 --- a/common/net/src/msg/client.rs +++ b/common/net/src/msg/client.rs @@ -36,6 +36,7 @@ pub enum ClientType { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ClientRegister { pub token_or_username: String, + pub locale: Option, } /// Messages sent from the client to the server diff --git a/common/net/src/msg/server.rs b/common/net/src/msg/server.rs index 9bc05e3206..b901d284e7 100644 --- a/common/net/src/msg/server.rs +++ b/common/net/src/msg/server.rs @@ -47,10 +47,14 @@ pub enum ServerMsg { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServerInfo { pub name: String, - pub description: String, pub git_hash: String, pub git_date: String, pub auth_provider: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ServerDescription { + pub motd: String, pub rules: Option, } @@ -70,6 +74,7 @@ pub enum ServerInit { material_stats: MaterialStatManifest, ability_map: comp::item::tool::AbilityMap, server_constants: ServerConstants, + description: ServerDescription, }, } diff --git a/common/src/cmd.rs b/common/src/cmd.rs index b26059550e..7a6c9121e3 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -651,9 +651,7 @@ impl ServerChatCommand { "Make a sprite at your location", Some(Admin), ), - ServerChatCommand::Motd => { - cmd(vec![Message(Optional)], "View the server description", None) - }, + ServerChatCommand::Motd => cmd(vec![], "View the server description", None), ServerChatCommand::Object => cmd( vec![Enum("object", OBJECTS.clone(), Required)], "Spawn an object", @@ -720,7 +718,7 @@ impl ServerChatCommand { Some(Moderator), ), ServerChatCommand::SetMotd => cmd( - vec![Message(Optional)], + vec![Any("locale", Optional), Message(Optional)], "Set the server description", Some(Admin), ), diff --git a/server/src/client.rs b/server/src/client.rs index d2a76bf44a..c56e2a5eac 100644 --- a/server/src/client.rs +++ b/server/src/client.rs @@ -15,6 +15,7 @@ pub struct Client { pub participant: Option, pub last_ping: f64, pub login_msg_sent: AtomicBool, + pub locale: Option, //TODO: Consider splitting each of these out into their own components so all the message //processing systems can run in parallel with each other (though it may turn out not to @@ -48,6 +49,7 @@ impl Client { client_type: ClientType, participant: Participant, last_ping: f64, + locale: Option, general_stream: Stream, ping_stream: Stream, register_stream: Stream, @@ -65,6 +67,7 @@ impl Client { client_type, participant: Some(participant), last_ping, + locale, login_msg_sent: AtomicBool::new(false), general_stream, ping_stream, diff --git a/server/src/cmd.rs b/server/src/cmd.rs index d4eca8ae8d..b82896d52d 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -6,7 +6,8 @@ use crate::{ location::Locations, login_provider::LoginProvider, settings::{ - Ban, BanAction, BanInfo, EditableSetting, SettingError, WhitelistInfo, WhitelistRecord, + server_description::ServerDescription, Ban, BanAction, BanInfo, EditableSetting, + SettingError, WhitelistInfo, WhitelistRecord, }, sys::terrain::NpcData, weather::WeatherSim, @@ -775,11 +776,23 @@ fn handle_motd( _args: Vec, _action: &ServerChatCommand, ) -> CmdResult<()> { + let locale = server + .state + .ecs() + .read_storage::() + .get(client) + .and_then(|client| client.locale.clone()); + server.notify_client( client, ServerGeneral::server_msg( ChatType::CommandInfo, - server.editable_settings().server_description.motd.clone(), + server + .editable_settings() + .server_description + .get(locale.as_ref()) + .map_or("", |d| &d.motd) + .to_string(), ), ); Ok(()) @@ -790,22 +803,31 @@ fn handle_set_motd( client: EcsEntity, _target: EcsEntity, args: Vec, - _action: &ServerChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { let data_dir = server.data_dir(); let client_uuid = uuid(server, client, "client")?; // Ensure the person setting this has a real role in the settings file, since // it's persistent. let _client_real_role = real_role(server, client_uuid, "client")?; - match parse_cmd_args!(args, String) { - Some(msg) => { + match parse_cmd_args!(args, String, String) { + (Some(locale), Some(msg)) => { let edit = server .editable_settings_mut() .server_description .edit(data_dir.as_ref(), |d| { let info = format!("Server message of the day set to {:?}", msg); - d.motd = msg; + + if let Some(description) = d.descriptions.get_mut(&locale) { + description.motd = msg; + } else { + d.descriptions.insert(locale, ServerDescription { + motd: msg, + rules: None, + }); + } + Some(info) }); drop(data_dir); @@ -813,20 +835,25 @@ fn handle_set_motd( unreachable!("edit always returns Some") }) }, - None => { + (Some(locale), None) => { let edit = server .editable_settings_mut() .server_description .edit(data_dir.as_ref(), |d| { - d.motd.clear(); - Some("Removed server message of the day".to_string()) + if let Some(description) = d.descriptions.get_mut(&locale) { + description.motd.clear(); + Some("Removed server message of the day".to_string()) + } else { + Some("This locale had no motd set".to_string()) + } }); drop(data_dir); edit_setting_feedback(server, client, edit, || { unreachable!("edit always returns Some") }) }, + _ => Err(Content::Plain(action.help_string())), } } diff --git a/server/src/connection_handler.rs b/server/src/connection_handler.rs index 93effcab90..bf2091d6ad 100644 --- a/server/src/connection_handler.rs +++ b/server/src/connection_handler.rs @@ -146,6 +146,7 @@ impl ConnectionHandler { client_type, participant, server_data.time, + None, general_stream, ping_stream, register_stream, diff --git a/server/src/lib.rs b/server/src/lib.rs index 5f17aec6e1..24bd2877ab 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -624,14 +624,12 @@ impl Server { pub fn get_server_info(&self) -> ServerInfo { let settings = self.state.ecs().fetch::(); - let editable_settings = self.state.ecs().fetch::(); + ServerInfo { name: settings.server_name.clone(), - description: editable_settings.server_description.motd.clone(), git_hash: common::util::GIT_HASH.to_string(), git_date: common::util::GIT_DATE.to_string(), auth_provider: settings.auth_server_address.clone(), - rules: editable_settings.server_description.rules.clone(), } } diff --git a/server/src/settings.rs b/server/src/settings.rs index 3fe2f1653b..288c7e9d03 100644 --- a/server/src/settings.rs +++ b/server/src/settings.rs @@ -10,7 +10,7 @@ pub use admin::{AdminRecord, Admins}; pub use banlist::{ Ban, BanAction, BanEntry, BanError, BanErrorKind, BanInfo, BanKind, BanRecord, Banlist, }; -pub use server_description::ServerDescription; +pub use server_description::ServerDescriptions; pub use whitelist::{Whitelist, WhitelistInfo, WhitelistRecord}; use chrono::Utc; @@ -362,7 +362,7 @@ const MIGRATION_UPGRADE_GUARANTEE: &str = "Any valid file of an old verison shou pub struct EditableSettings { pub whitelist: Whitelist, pub banlist: Banlist, - pub server_description: ServerDescription, + pub server_description: ServerDescriptions, pub admins: Admins, } @@ -371,7 +371,7 @@ impl EditableSettings { Self { whitelist: Whitelist::load(data_dir), banlist: Banlist::load(data_dir), - server_description: ServerDescription::load(data_dir), + server_description: ServerDescriptions::load(data_dir), admins: Admins::load(data_dir), } } @@ -379,8 +379,13 @@ impl EditableSettings { pub fn singleplayer(data_dir: &Path) -> Self { let load = Self::load(data_dir); - let mut server_description = ServerDescription::default(); - server_description.motd = "Who needs friends anyway?".into(); + let mut server_description = ServerDescriptions::default(); + server_description + .descriptions + .values_mut() + .for_each(|entry| { + entry.motd = "Who needs friends anyway?".into(); + }); let mut admins = Admins::default(); // TODO: Let the player choose if they want to use admin commands or not diff --git a/server/src/settings/server_description.rs b/server/src/settings/server_description.rs index fba5e9735f..0fa28bc6ec 100644 --- a/server/src/settings/server_description.rs +++ b/server/src/settings/server_description.rs @@ -20,22 +20,22 @@ pub use self::v2::*; pub enum ServerDescriptionRaw { V0(v0::ServerDescription), V1(v1::ServerDescription), - V2(ServerDescription), + V2(ServerDescriptions), } -impl From for ServerDescriptionRaw { - fn from(value: ServerDescription) -> Self { +impl From for ServerDescriptionRaw { + fn from(value: ServerDescriptions) -> Self { // Replace variant with that of current latest version. Self::V2(value) } } -impl TryFrom for (Version, ServerDescription) { - type Error = ::Error; +impl TryFrom for (Version, ServerDescriptions) { + type Error = ::Error; fn try_from( value: ServerDescriptionRaw, - ) -> Result::Error> { + ) -> Result::Error> { use ServerDescriptionRaw::*; Ok(match value { // Old versions @@ -48,9 +48,9 @@ impl TryFrom for (Version, ServerDescription) { } } -type Final = ServerDescription; +type Final = ServerDescriptions; -impl EditableSetting for ServerDescription { +impl EditableSetting for ServerDescriptions { type Error = Infallible; type Legacy = legacy::ServerDescription; type Setting = ServerDescriptionRaw; @@ -169,30 +169,46 @@ mod v1 { } } - use super::{v2 as next, MIGRATION_UPGRADE_GUARANTEE}; + use super::v2 as next; impl TryFrom for Final { type Error = ::Error; fn try_from(mut value: ServerDescription) -> Result { value.validate()?; - Ok(next::ServerDescription::migrate(value) - .try_into() - .expect(MIGRATION_UPGRADE_GUARANTEE)) + Ok(next::ServerDescriptions::migrate(value)) } } } mod v2 { + use std::collections::HashMap; + use super::{v1 as prev, Final}; use crate::settings::editable::{EditableSetting, Version}; use serde::{Deserialize, Serialize}; + /// Map of all localized [`ServerDescription`]s + #[derive(Clone, Deserialize, Serialize)] + pub struct ServerDescriptions { + pub default_locale: String, + pub descriptions: HashMap, + } + #[derive(Clone, Deserialize, Serialize)] pub struct ServerDescription { pub motd: String, pub rules: Option, } + impl Default for ServerDescriptions { + fn default() -> Self { + Self { + default_locale: "en".to_string(), + descriptions: HashMap::from([("en".to_string(), ServerDescription::default())]), + } + } + } + impl Default for ServerDescription { fn default() -> Self { Self { @@ -202,14 +218,31 @@ mod v2 { } } - impl ServerDescription { + impl ServerDescriptions { + pub fn get(&self, locale: Option<&String>) -> Option<&ServerDescription> { + let locale = locale.map_or(&self.default_locale, |locale| { + if self.descriptions.contains_key(locale) { + locale + } else { + &self.default_locale + } + }); + + self.descriptions.get(locale) + } + } + + impl ServerDescriptions { /// One-off migration from the previous version. This must be /// guaranteed to produce a valid settings file as long as it is /// called with a valid settings file from the previous version. pub(super) fn migrate(prev: prev::ServerDescription) -> Self { Self { - motd: prev.0, - rules: None, + default_locale: "en".to_string(), + descriptions: HashMap::from([("en".to_string(), ServerDescription { + motd: prev.0, + rules: None, + })]), } } @@ -220,7 +253,22 @@ mod v2 { /// been modified during validation (this is why validate takes /// `&mut self`). pub(super) fn validate(&mut self) -> Result::Error> { - Ok(Version::Latest) + if self.descriptions.is_empty() { + *self = Self::default(); + Ok(Version::Old) + } else if !self.descriptions.contains_key(&self.default_locale) { + // default locale not present, select the a random one (as ordering in hashmaps + // isn't predictable) + self.default_locale = self + .descriptions + .keys() + .next() + .expect("We know descriptions isn't empty") + .to_string(); + Ok(Version::Old) + } else { + Ok(Version::Latest) + } } } diff --git a/server/src/sys/msg/character_screen.rs b/server/src/sys/msg/character_screen.rs index ccbed7802e..42b9067fd6 100644 --- a/server/src/sys/msg/character_screen.rs +++ b/server/src/sys/msg/character_screen.rs @@ -45,10 +45,13 @@ impl Sys { ) -> Result<(), crate::error::Error> { let mut send_join_messages = || -> Result<(), crate::error::Error> { // Give the player a welcome message - if !editable_settings.server_description.motd.is_empty() { + let localized_description = editable_settings + .server_description + .get(client.locale.as_ref()); + if !localized_description.map_or(true, |d| d.motd.is_empty()) { client.send(ServerGeneral::server_msg( ChatType::CommandInfo, - editable_settings.server_description.motd.as_str(), + localized_description.map_or("", |d| &d.motd), ))?; } diff --git a/server/src/sys/msg/register.rs b/server/src/sys/msg/register.rs index 09ef22e801..94241c6fc9 100644 --- a/server/src/sys/msg/register.rs +++ b/server/src/sys/msg/register.rs @@ -16,8 +16,8 @@ use common::{ use common_base::prof_span; use common_ecs::{Job, Origin, Phase, System}; use common_net::msg::{ - CharacterInfo, ClientRegister, DisconnectReason, PlayerInfo, PlayerListUpdate, RegisterError, - ServerGeneral, ServerInit, WorldMapMsg, + server::ServerDescription, CharacterInfo, ClientRegister, DisconnectReason, PlayerInfo, + PlayerListUpdate, RegisterError, ServerGeneral, ServerInit, WorldMapMsg, }; use hashbrown::{hash_map, HashMap}; use itertools::Either; @@ -129,12 +129,20 @@ impl<'a> System<'a> for Sys { // defer auth lockup for (entity, client) in (&read_data.entities, &mut clients).join() { + let mut locale = None; + let _ = super::try_recv_all(client, 0, |_, msg: ClientRegister| { trace!(?msg.token_or_username, "defer auth lockup"); let pending = read_data.login_provider.verify(&msg.token_or_username); + locale = msg.locale; let _ = pending_logins.insert(entity, pending); Ok(()) }); + + // Update locale + if let Some(locale) = locale { + client.locale = Some(locale); + } } let old_player_count = player_list.len(); @@ -320,6 +328,19 @@ impl<'a> System<'a> for Sys { // Tell the client its request was successful. client.send(Ok(()))?; + + let description = read_data + .editable_settings + .server_description + .get(client.locale.as_ref()) + .map(|description| + ServerDescription { + motd: description.motd.clone(), + rules: description.rules.clone() + } + ) + .unwrap_or_default(); + // Send client all the tracked components currently attached to its entity // as well as synced resources (currently only `TimeOfDay`) debug!("Starting initial sync with client."); @@ -340,6 +361,7 @@ impl<'a> System<'a> for Sys { server_constants: ServerConstants { day_cycle_coefficient: read_data.settings.day_cycle_coefficient() }, + description, })?; debug!("Done initial sync with client."); diff --git a/voxygen/src/hud/settings_window/language.rs b/voxygen/src/hud/settings_window/language.rs index bcde69f66e..c942c0fa10 100644 --- a/voxygen/src/hud/settings_window/language.rs +++ b/voxygen/src/hud/settings_window/language.rs @@ -17,6 +17,8 @@ widget_ids! { window_r, english_fallback_button, english_fallback_button_label, + share_with_server_checkbox, + share_with_server_checkbox_label, window_scrollbar, language_list[], } @@ -86,9 +88,64 @@ impl<'a> Widget for Language<'a> { .rgba(0.33, 0.33, 0.33, 1.0) .set(state.ids.window_scrollbar, ui); + // Share with server button + let share_with_server = ToggleButton::new( + self.global_state.settings.language.share_with_server, + self.imgs.checkbox, + self.imgs.checkbox_checked, + ) + .w_h(18.0, 18.0) + .top_left_with_margin_on(state.ids.window, 20.0) + .hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo) + .press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked) + .set(state.ids.share_with_server_checkbox, ui); + + if share_with_server != self.global_state.settings.language.share_with_server { + events.push(ToggleShareWithServer(share_with_server)); + } + + Text::new( + &self + .localized_strings + .get_msg("hud-settings-language_share_with_server"), + ) + .right_from(state.ids.share_with_server_checkbox, 10.0) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .graphics_for(state.ids.share_with_server_checkbox) + .color(TEXT_COLOR) + .set(state.ids.share_with_server_checkbox_label, ui); + + // English as fallback language + let show_english_fallback = ToggleButton::new( + self.global_state.settings.language.use_english_fallback, + self.imgs.checkbox, + self.imgs.checkbox_checked, + ) + .w_h(18.0, 18.0) + .down_from(state.ids.share_with_server_checkbox, 10.0) + .hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo) + .press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked) + .set(state.ids.english_fallback_button, ui); + + if self.global_state.settings.language.use_english_fallback != show_english_fallback { + events.push(ToggleEnglishFallback(show_english_fallback)); + } + + Text::new( + &self + .localized_strings + .get_msg("hud-settings-english_fallback"), + ) + .right_from(state.ids.english_fallback_button, 10.0) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .graphics_for(state.ids.english_fallback_button) + .color(TEXT_COLOR) + .set(state.ids.english_fallback_button_label, ui); + // List available languages let selected_language = &self.global_state.settings.language.selected_language; - let english_fallback = self.global_state.settings.language.use_english_fallback; let language_list = list_localizations(); if state.ids.language_list.len() < language_list.len() { state.update(|state| { @@ -107,7 +164,7 @@ impl<'a> Widget for Language<'a> { self.imgs.nothing }); let button = if i == 0 { - button.mid_top_with_margin_on(state.ids.window, 20.0) + button.mid_top_with_margin_on(state.ids.window, 58.0) } else { button.mid_bottom_with_margin_on(state.ids.language_list[i - 1], -button_h) }; @@ -127,40 +184,6 @@ impl<'a> Widget for Language<'a> { } } - // English as fallback language - let show_english_fallback = ToggleButton::new( - english_fallback, - self.imgs.checkbox, - self.imgs.checkbox_checked, - ) - .w_h(18.0, 18.0); - let show_english_fallback = if let Some(id) = state.ids.language_list.last() { - show_english_fallback.down_from(*id, 8.0) - //mid_bottom_with_margin_on(id, -button_h) - } else { - show_english_fallback.mid_top_with_margin_on(state.ids.window, 20.0) - }; - let show_english_fallback = show_english_fallback - .hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo) - .press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked) - .set(state.ids.english_fallback_button, ui); - - if english_fallback != show_english_fallback { - events.push(ToggleEnglishFallback(show_english_fallback)); - } - - Text::new( - &self - .localized_strings - .get_msg("hud-settings-english_fallback"), - ) - .right_from(state.ids.english_fallback_button, 10.0) - .font_size(self.fonts.cyri.scale(14)) - .font_id(self.fonts.cyri.conrod_id) - .graphics_for(state.ids.english_fallback_button) - .color(TEXT_COLOR) - .set(state.ids.english_fallback_button_label, ui); - events } } diff --git a/voxygen/src/menu/char_selection/mod.rs b/voxygen/src/menu/char_selection/mod.rs index 8de9102655..02b86ee6d3 100644 --- a/voxygen/src/menu/char_selection/mod.rs +++ b/voxygen/src/menu/char_selection/mod.rs @@ -1,6 +1,7 @@ mod ui; use crate::{ + menu::{main::rand_bg_image_spec, server_info::ServerInfoState}, render::{Drawer, GlobalsBindGroup}, scene::simple::{self as scene, Scene}, session::SessionState, @@ -160,6 +161,28 @@ impl PlayState for CharSelectionState { Rc::clone(&self.client), ))); }, + ui::Event::ShowRules => { + let client = self.client.borrow(); + + let server_info = client.server_info().clone(); + let server_description = client.server_description().clone(); + + let char_select = + CharSelectionState::new(global_state, Rc::clone(&self.client)); + + let new_state = ServerInfoState::try_from_server_info( + global_state, + rand_bg_image_spec(), + char_select, + server_info, + server_description, + true, + ) + .map(|s| Box::new(s) as _) + .unwrap_or_else(|s| Box::new(s) as _); + + return PlayStateResult::Switch(new_state); + }, ui::Event::ClearCharacterListError => { self.char_selection_ui.error = None; }, diff --git a/voxygen/src/menu/char_selection/ui/mod.rs b/voxygen/src/menu/char_selection/ui/mod.rs index fac2523333..939f25fa62 100644 --- a/voxygen/src/menu/char_selection/ui/mod.rs +++ b/voxygen/src/menu/char_selection/ui/mod.rs @@ -153,6 +153,7 @@ pub enum Event { DeleteCharacter(CharacterId), ClearCharacterListError, SelectCharacter(Option), + ShowRules, } enum Mode { @@ -163,6 +164,7 @@ enum Mode { character_buttons: Vec, new_character_button: button::State, logout_button: button::State, + rule_button: button::State, enter_world_button: button::State, spectate_button: button::State, yes_button: button::State, @@ -204,6 +206,7 @@ impl Mode { character_buttons: Vec::new(), new_character_button: Default::default(), logout_button: Default::default(), + rule_button: Default::default(), enter_world_button: Default::default(), spectate_button: Default::default(), yes_button: Default::default(), @@ -308,12 +311,14 @@ struct Controls { map_img: GraphicId, possible_starting_sites: Vec, world_sz: Vec2, + has_rules: bool, } #[derive(Clone)] enum Message { Back, Logout, + ShowRules, EnterWorld, Spectate, Select(CharacterId), @@ -356,6 +361,7 @@ impl Controls { map_img: GraphicId, possible_starting_sites: Vec, world_sz: Vec2, + has_rules: bool, ) -> Self { let version = common::util::DISPLAY_VERSION_LONG.clone(); let alpha = format!("Veloren {}", common::util::DISPLAY_VERSION.as_str()); @@ -377,6 +383,7 @@ impl Controls { map_img, possible_starting_sites, world_sz, + has_rules, } } @@ -467,6 +474,7 @@ impl Controls { ref mut character_buttons, ref mut new_character_button, ref mut logout_button, + ref mut rule_button, ref mut enter_world_button, ref mut spectate_button, ref mut yes_button, @@ -738,7 +746,25 @@ impl Controls { ]) .height(Length::Fill); - let left_column = Column::with_children(vec![server.into(), characters.into()]) + let mut left_column_children = vec![server.into(), characters.into()]; + + if self.has_rules { + left_column_children.push( + Container::new(neat_button( + rule_button, + i18n.get_msg("char_selection-rules").into_owned(), + FILL_FRAC_ONE, + button_style, + Some(Message::ShowRules), + )) + .align_y(Align::End) + .width(Length::Fill) + .center_x() + .height(Length::Units(52)) + .into(), + ); + } + let left_column = Column::with_children(left_column_children) .spacing(10) .width(Length::Units(322)) // TODO: see if we can get iced to work with settings below // .max_width(360) @@ -1670,6 +1696,9 @@ impl Controls { Message::Logout => { events.push(Event::Logout); }, + Message::ShowRules => { + events.push(Event::ShowRules); + }, Message::ConfirmDeletion => { if let Mode::Select { info_content, .. } = &mut self.mode { if let Some(InfoContent::Deletion(idx)) = info_content { @@ -1997,6 +2026,7 @@ impl CharSelectionUi { .map(|info| info.site.clone()) .collect(), client.world_data().chunk_size().as_(), + client.server_description().rules.is_some(), ); Self { diff --git a/voxygen/src/menu/main/client_init.rs b/voxygen/src/menu/main/client_init.rs index c1fcc57819..95f8821690 100644 --- a/voxygen/src/menu/main/client_init.rs +++ b/voxygen/src/menu/main/client_init.rs @@ -49,6 +49,7 @@ impl ClientInit { username: String, password: String, runtime: Arc, + locale: Option, ) -> Self { let (tx, rx) = unbounded(); let (trust_tx, trust_rx) = unbounded(); @@ -81,6 +82,7 @@ impl ClientInit { &mut mismatched_server_info, &username, &password, + locale.clone(), trust_fn, &|stage| { let _ = init_stage_tx.send(stage); diff --git a/voxygen/src/menu/main/mod.rs b/voxygen/src/menu/main/mod.rs index 9d26fc2a51..6f3c99de0e 100644 --- a/voxygen/src/menu/main/mod.rs +++ b/voxygen/src/menu/main/mod.rs @@ -26,6 +26,8 @@ use tokio::runtime; use tracing::error; use ui::{Event as MainMenuEvent, MainMenuUi}; +pub use ui::rand_bg_image_spec; + #[derive(Debug)] pub enum DetailedInitializationStage { #[cfg(feature = "singleplayer")] @@ -123,6 +125,9 @@ impl PlayState for MainMenuState { ConnectionArgs::Mpsc(14004), &mut self.init, &global_state.tokio_runtime, + global_state.settings.language.share_with_server.then_some( + global_state.settings.language.selected_language.clone(), + ), &global_state.i18n, ); }, @@ -294,6 +299,7 @@ impl PlayState for MainMenuState { self.main_menu_ui.connected(); let server_info = client.server_info().clone(); + let server_description = client.server_description().clone(); let char_select = CharSelectionState::new( global_state, @@ -305,6 +311,8 @@ impl PlayState for MainMenuState { self.main_menu_ui.bg_img_spec(), char_select, server_info, + server_description, + false, ) .map(|s| Box::new(s) as _) .unwrap_or_else(|s| Box::new(s) as _); @@ -354,6 +362,11 @@ impl PlayState for MainMenuState { connection_args, &mut self.init, &global_state.tokio_runtime, + global_state + .settings + .language + .share_with_server + .then_some(global_state.settings.language.selected_language.clone()), &global_state.i18n, ); }, @@ -584,6 +597,7 @@ fn attempt_login( connection_args: ConnectionArgs, init: &mut InitState, runtime: &Arc, + locale: Option, localized_strings: &LocalizationHandle, ) { let localization = localized_strings.read(); @@ -616,6 +630,7 @@ fn attempt_login( username, password, Arc::clone(runtime), + locale, )); } } diff --git a/voxygen/src/menu/main/ui/mod.rs b/voxygen/src/menu/main/ui/mod.rs index 13f5cb64b0..e7fa731073 100644 --- a/voxygen/src/menu/main/ui/mod.rs +++ b/voxygen/src/menu/main/ui/mod.rs @@ -696,7 +696,7 @@ impl MainMenuUi { let fonts = Fonts::load(i18n.fonts(), &mut ui).expect("Impossible to load fonts"); - let bg_img_spec = BG_IMGS.choose(&mut thread_rng()).unwrap(); + let bg_img_spec = rand_bg_image_spec(); let bg_img = assets::Image::load_expect(bg_img_spec).read().to_image(); let controls = Controls::new( @@ -814,3 +814,5 @@ impl MainMenuUi { pub fn render<'a>(&'a self, drawer: &mut UiDrawer<'_, 'a>) { self.ui.render(drawer); } } + +pub fn rand_bg_image_spec() -> &'static str { BG_IMGS.choose(&mut thread_rng()).unwrap() } diff --git a/voxygen/src/menu/server_info/mod.rs b/voxygen/src/menu/server_info/mod.rs index 3aa494d27a..9ff08f298d 100644 --- a/voxygen/src/menu/server_info/mod.rs +++ b/voxygen/src/menu/server_info/mod.rs @@ -17,6 +17,7 @@ use common::{ comp, }; use common_base::span; +use common_net::msg::server::ServerDescription; use i18n::LocalizationHandle; use iced::{ button, scrollable, Align, Column, Container, HorizontalAlignment, Length, Row, Scrollable, @@ -47,7 +48,8 @@ pub struct Controls { decline_button: button::State, scrollable: scrollable::State, server_info: ServerInfo, - seen_before: bool, + server_description: ServerDescription, + changed: bool, } pub struct ServerInfoState { @@ -76,15 +78,18 @@ impl ServerInfoState { bg_img_spec: &'static str, char_select: CharSelectionState, server_info: ServerInfo, + server_description: ServerDescription, + force_show: bool, ) -> Result { let server = global_state.profile.servers.get(&server_info.name); // If there are no rules, or we've already accepted these rules, we don't need // this state - if server_info.rules.is_none() + if (server_description.rules.is_none() || server.map_or(false, |s| { - s.accepted_rules == Some(rules_hash(&server_info.rules)) - }) + s.accepted_rules == Some(rules_hash(&server_description.rules)) + })) + && !force_show { return Err(char_select); } @@ -101,6 +106,11 @@ impl ServerInfoState { ) .unwrap(); + let changed = server.map_or(false, |s| { + s.accepted_rules + .is_some_and(|accepted| accepted != rules_hash(&server_description.rules)) + }); + Ok(Self { scene: Scene::new(global_state.window.renderer_mut()), controls: Controls { @@ -110,12 +120,13 @@ impl ServerInfoState { )), imgs: Imgs::load(&mut ui).expect("Failed to load images"), fonts: Fonts::load(i18n.fonts(), &mut ui).expect("Impossible to load fonts"), - i18n: global_state.i18n.clone(), + i18n: global_state.i18n, accept_button: Default::default(), decline_button: Default::default(), scrollable: Default::default(), server_info, - seen_before: server.map_or(false, |s| s.accepted_rules.is_some()), + server_description, + changed, }, ui, char_select: Some(char_select), @@ -191,6 +202,7 @@ impl PlayState for ServerInfoState { &mut global_state.clipboard, ); + #[allow(clippy::never_loop)] // TODO: Remove when more message types are added for message in messages { match message { Message::Accept => { @@ -200,7 +212,8 @@ impl PlayState for ServerInfoState { .servers .get_mut(&self.controls.server_info.name) { - server.accepted_rules = Some(rules_hash(&self.controls.server_info.rules)); + server.accepted_rules = + Some(rules_hash(&self.controls.server_description.rules)); } return PlayStateResult::Switch(Box::new(self.char_select.take().unwrap())); @@ -277,18 +290,18 @@ impl Controls { elements.push( Container::new( iced::Text::new(i18n.get_msg("main-server-rules")) - .size(self.fonts.cyri.scale(30)) + .size(self.fonts.cyri.scale(36)) .horizontal_alignment(HorizontalAlignment::Center), ) .width(Length::Fill) .into(), ); - if self.seen_before { + if self.changed { elements.push( Container::new( iced::Text::new(i18n.get_msg("main-server-rules-seen-before")) - .size(self.fonts.cyri.scale(20)) + .size(self.fonts.cyri.scale(30)) .color(IMPORTANT_TEXT_COLOR) .horizontal_alignment(HorizontalAlignment::Center), ) @@ -306,11 +319,16 @@ impl Controls { elements.push( Scrollable::new(&mut self.scrollable) .push( - iced::Text::new(self.server_info.rules.as_deref().unwrap_or("")) - .size(self.fonts.cyri.scale(16)) - .width(Length::Shrink) - .horizontal_alignment(HorizontalAlignment::Left) - .vertical_alignment(VerticalAlignment::Top), + iced::Text::new( + self.server_description + .rules + .as_deref() + .unwrap_or(""), + ) + .size(self.fonts.cyri.scale(26)) + .width(Length::Shrink) + .horizontal_alignment(HorizontalAlignment::Left) + .vertical_alignment(VerticalAlignment::Top), ) .height(Length::Fill) .width(Length::Fill) diff --git a/voxygen/src/session/settings_change.rs b/voxygen/src/session/settings_change.rs index c624996aeb..fa37d51d67 100644 --- a/voxygen/src/session/settings_change.rs +++ b/voxygen/src/session/settings_change.rs @@ -161,6 +161,7 @@ pub enum Interface { #[derive(Clone)] pub enum Language { ChangeLanguage(Box), + ToggleShareWithServer(bool), ToggleEnglishFallback(bool), } #[derive(Clone)] @@ -717,6 +718,9 @@ impl SettingsChange { .i18n .set_english_fallback(settings.language.use_english_fallback); }, + Language::ToggleShareWithServer(share) => { + settings.language.share_with_server = share; + }, }, SettingsChange::Networking(networking_change) => match networking_change { Networking::AdjustTerrainViewDistance(terrain_vd) => { diff --git a/voxygen/src/settings/language.rs b/voxygen/src/settings/language.rs index 28fd1840e3..0f062c9e03 100644 --- a/voxygen/src/settings/language.rs +++ b/voxygen/src/settings/language.rs @@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize}; #[serde(default)] pub struct LanguageSettings { pub selected_language: String, + #[serde(default = "default_true")] + pub share_with_server: bool, pub use_english_fallback: bool, } @@ -11,7 +13,10 @@ impl Default for LanguageSettings { fn default() -> Self { Self { selected_language: i18n::REFERENCE_LANG.to_string(), + share_with_server: true, use_english_fallback: true, } } } + +fn default_true() -> bool { true }