mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Server rules i18n and rules button in character screen
This commit is contained in:
parent
e4dbe50f39
commit
d6371f7f9b
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
&|_| {},
|
||||
|_| {},
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
),
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
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())),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,6 +146,7 @@ impl ConnectionHandler {
|
||||
client_type,
|
||||
participant,
|
||||
server_data.time,
|
||||
None,
|
||||
general_stream,
|
||||
ping_stream,
|
||||
register_stream,
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
default_locale: "en".to_string(),
|
||||
descriptions: HashMap::from([("en".to_string(), ServerDescription {
|
||||
motd: prev.0,
|
||||
rules: None,
|
||||
})]),
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,9 +253,24 @@ 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> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Whenever there is a version upgrade, copy this note as well as the
|
||||
// commented-out code below to the next version, then uncomment the code
|
||||
|
@ -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),
|
||||
))?;
|
||||
}
|
||||
|
||||
|
@ -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.");
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
},
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -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() }
|
||||
|
@ -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,8 +319,13 @@ 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))
|
||||
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),
|
||||
|
@ -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) => {
|
||||
|
@ -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 }
|
||||
|
Loading…
Reference in New Issue
Block a user