Server rules i18n and rules button in character screen

This commit is contained in:
Maxicarlos08 2024-01-14 23:38:02 +01:00
parent e4dbe50f39
commit d6371f7f9b
No known key found for this signature in database
23 changed files with 345 additions and 96 deletions

View File

@ -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

View File

@ -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

View File

@ -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<PresenceKind>,
runtime: Arc<Runtime>,
server_info: ServerInfo,
/// Localized server motd and rules
server_description: ServerDescription,
world_data: WorldData,
weather: WeatherLerp,
player_list: HashMap<Uid, PlayerInfo>,
@ -305,6 +308,7 @@ impl Client {
mismatched_server_info: &mut Option<ServerInfo>,
username: &str,
password: &str,
locale: Option<String>,
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<String>,
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::<ServerRegisterAnswer>().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,
&|_| {},
|_| {},

View File

@ -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<String>,
}
/// Messages sent from the client to the server

View File

@ -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<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ServerDescription {
pub motd: String,
pub rules: Option<String>,
}
@ -70,6 +74,7 @@ pub enum ServerInit {
material_stats: MaterialStatManifest,
ability_map: comp::item::tool::AbilityMap,
server_constants: ServerConstants,
description: ServerDescription,
},
}

View File

@ -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),
),

View File

@ -15,6 +15,7 @@ pub struct Client {
pub participant: Option<Participant>,
pub last_ping: f64,
pub login_msg_sent: AtomicBool,
pub locale: Option<String>,
//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<String>,
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,

View File

@ -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<String>,
_action: &ServerChatCommand,
) -> CmdResult<()> {
let locale = server
.state
.ecs()
.read_storage::<Client>()
.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<String>,
_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())),
}
}

View File

@ -146,6 +146,7 @@ impl ConnectionHandler {
client_type,
participant,
server_data.time,
None,
general_stream,
ping_stream,
register_stream,

View File

@ -624,14 +624,12 @@ impl Server {
pub fn get_server_info(&self) -> ServerInfo {
let settings = self.state.ecs().fetch::<Settings>();
let editable_settings = self.state.ecs().fetch::<EditableSettings>();
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(),
}
}

View File

@ -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

View File

@ -20,22 +20,22 @@ pub use self::v2::*;
pub enum ServerDescriptionRaw {
V0(v0::ServerDescription),
V1(v1::ServerDescription),
V2(ServerDescription),
V2(ServerDescriptions),
}
impl From<ServerDescription> for ServerDescriptionRaw {
fn from(value: ServerDescription) -> Self {
impl From<ServerDescriptions> for ServerDescriptionRaw {
fn from(value: ServerDescriptions) -> Self {
// Replace variant with that of current latest version.
Self::V2(value)
}
}
impl TryFrom<ServerDescriptionRaw> for (Version, ServerDescription) {
type Error = <ServerDescription as EditableSetting>::Error;
impl TryFrom<ServerDescriptionRaw> for (Version, ServerDescriptions) {
type Error = <ServerDescriptions as EditableSetting>::Error;
fn try_from(
value: ServerDescriptionRaw,
) -> Result<Self, <ServerDescription as EditableSetting>::Error> {
) -> Result<Self, <ServerDescriptions as EditableSetting>::Error> {
use ServerDescriptionRaw::*;
Ok(match value {
// Old versions
@ -48,9 +48,9 @@ impl TryFrom<ServerDescriptionRaw> 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<ServerDescription> for Final {
type Error = <Final as EditableSetting>::Error;
fn try_from(mut value: ServerDescription) -> Result<Final, Self::Error> {
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<String, ServerDescription>,
}
#[derive(Clone, Deserialize, Serialize)]
pub struct ServerDescription {
pub motd: String,
pub rules: Option<String>,
}
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<Version, <Final as EditableSetting>::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)
}
}
}

View File

@ -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),
))?;
}

View File

@ -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.");

View File

@ -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
}
}

View File

@ -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;
},

View File

@ -153,6 +153,7 @@ pub enum Event {
DeleteCharacter(CharacterId),
ClearCharacterListError,
SelectCharacter(Option<CharacterId>),
ShowRules,
}
enum Mode {
@ -163,6 +164,7 @@ enum Mode {
character_buttons: Vec<button::State>,
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<SiteInfo>,
world_sz: Vec2<u32>,
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<SiteInfo>,
world_sz: Vec2<u32>,
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 {

View File

@ -49,6 +49,7 @@ impl ClientInit {
username: String,
password: String,
runtime: Arc<runtime::Runtime>,
locale: Option<String>,
) -> 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);

View File

@ -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<runtime::Runtime>,
locale: Option<String>,
localized_strings: &LocalizationHandle,
) {
let localization = localized_strings.read();
@ -616,6 +630,7 @@ fn attempt_login(
username,
password,
Arc::clone(runtime),
locale,
));
}
}

View File

@ -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() }

View File

@ -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<Self, CharSelectionState> {
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("<rules>"))
.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("<rules>"),
)
.size(self.fonts.cyri.scale(26))
.width(Length::Shrink)
.horizontal_alignment(HorizontalAlignment::Left)
.vertical_alignment(VerticalAlignment::Top),
)
.height(Length::Fill)
.width(Length::Fill)

View File

@ -161,6 +161,7 @@ pub enum Interface {
#[derive(Clone)]
pub enum Language {
ChangeLanguage(Box<LanguageMetadata>),
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) => {

View File

@ -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 }