Merge branch 'zesterer/server-rules' into 'master'

Added an optional rules screen that requires accepting

See merge request veloren/veloren!4253
This commit is contained in:
Joshua Barretto 2024-01-16 17:14:57 +00:00
commit 2a13fe3228
30 changed files with 795 additions and 87 deletions

View File

@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Toggle for walking instead of running (Default: `I`).
- Added day duration slider configuration on map creation UI.
- Potion of Agility
- A way for servers to specify must-accept rules for players
### Changed
@ -95,7 +96,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed french translation "Énergie Consommée" -> "Regain d'Énergie"
- Fixed Perforate icon not displaying
- Make cave entrances easier to follow
- Renamed Twiggy Shoulders to match the Twig Armor set
- Renamed Twiggy Shoulders to match the Twig Armor set
- No longer stack buffs of the same kind with equal attributes, this could lead to a DoS if ie. an entity stayed long enough in lava.
## [0.15.0] - 2023-07-01

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_send_to_server = Send the configured language to servers (for localizing rules and motd messages)
hud-settings-awaitingkey = Press a key...
hud-settings-unbound = None
hud-settings-reset_keybinds = Reset to Defaults

View File

@ -82,6 +82,8 @@ main-servers-stream_error = Client connection/compression/(de)serialization erro
main-servers-database_error = Server database error: { $raw_error }
main-servers-persistence_error = Server persistence error (Probably Asset/Character Data related): { $raw_error }
main-servers-other_error = Server general error: { $raw_error }
main-server-rules = This server has rules that must be accepted
main-server-rules-seen-before = These rules have changed since the last time you accepted them
main-credits = Credits
main-credits-created_by = created by
main-credits-music = Music
@ -110,4 +112,4 @@ loading-tips =
.a18 = Need more bags or better armor to continue your journey? Press '{ $gameinput-crafting }' to open the crafting menu!
.a19 = Press '{ $gameinput-roll }' to roll. Rolling can be used to move faster and dodge enemy attacks.
.a20 = Wondering what an item is used for? Search 'input:<item name>' in crafting to see what recipes it's used in.
.a21 = You can take screenshots with '{ $gameinput-screenshot }'.
.a21 = You can take screenshots with '{ $gameinput-screenshot }'.

View File

@ -63,6 +63,7 @@ fn main() {
&mut None,
&username,
&password,
None,
|provider| provider == "https://auth.veloren.net",
&|_| {},
|_| {},

View File

@ -71,6 +71,7 @@ pub fn make_client(
server_info,
username,
password,
None,
|_| true,
&|_| {},
|_| {},

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,12 +47,17 @@ 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>,
}
/// Reponse To ClientType
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(clippy::large_enum_variant)]
@ -69,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).clone(),
server
.editable_settings()
.server_description
.get(locale.as_deref())
.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 description set to {:?}", msg);
**d = msg;
let info = format!("Server message of the day set to {:?}", 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.clear();
Some("Removed server description".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,10 +624,9 @@ 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).clone(),
git_hash: common::util::GIT_HASH.to_string(),
git_date: common::util::GIT_DATE.to_string(),
auth_provider: settings.auth_server_address.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;
@ -32,6 +32,8 @@ use std::{
use tracing::{error, warn};
use world::sim::FileOpts;
use self::server_description::ServerDescription;
const DEFAULT_WORLD_SEED: u32 = 230;
const CONFIG_DIR: &str = "server_config";
const SETTINGS_FILENAME: &str = "settings.ron";
@ -362,7 +364,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 +373,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 +381,13 @@ impl EditableSettings {
pub fn singleplayer(data_dir: &Path) -> Self {
let load = Self::load(data_dir);
let mut server_description = ServerDescription::default();
*server_description = "Who needs friends anyway?".into();
let mut server_description = ServerDescriptions::default();
server_description
.descriptions
.insert("en".to_string(), ServerDescription {
motd: "Who needs friends anyway?".to_string(),
rules: None,
});
let mut admins = Admins::default();
// TODO: Let the player choose if they want to use admin commands or not

View File

@ -12,43 +12,45 @@ use serde::{Deserialize, Serialize};
/// ServerDescription, the previously most recent module, and add a new module
/// for the latest version! Please respect the migration upgrade guarantee
/// found in the parent module with any upgrade.
pub use self::v1::*;
pub use self::v2::*;
/// Versioned settings files, one per version (v0 is only here as an example; we
/// do not expect to see any actual v0 settings files).
#[derive(Deserialize, Serialize)]
pub enum ServerDescriptionRaw {
V0(v0::ServerDescription),
V1(ServerDescription),
V1(v1::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::V1(value)
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
V0(value) => (Version::Old, value.try_into()?),
V1(value) => (Version::Old, value.try_into()?),
// Latest version (move to old section using the pattern of other old version when it
// is no longer latest).
V1(mut value) => (value.validate()?, value),
V2(mut value) => (value.validate()?, value),
})
}
}
type Final = ServerDescription;
type Final = ServerDescriptions;
impl EditableSetting for ServerDescription {
impl EditableSetting for ServerDescriptions {
type Error = Infallible;
type Legacy = legacy::ServerDescription;
type Setting = ServerDescriptionRaw;
@ -131,7 +133,6 @@ mod v1 {
use crate::settings::editable::{EditableSetting, Version};
use core::ops::{Deref, DerefMut};
use serde::{Deserialize, Serialize};
/* use super::v2 as next; */
#[derive(Clone, Deserialize, Serialize)]
#[serde(transparent)]
@ -168,10 +169,131 @@ mod v1 {
}
}
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::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 {
motd: "This is the best Veloren server".into(),
rules: None,
}
}
}
impl ServerDescriptions {
fn unwrap_locale_or_default<'a, 'b: 'a>(&'b self, locale: Option<&'a str>) -> &'a str {
locale.map_or(&self.default_locale, |locale| {
if self.descriptions.contains_key(locale) {
locale
} else {
&self.default_locale
}
})
}
pub fn get(&self, locale: Option<&str>) -> Option<&ServerDescription> {
self.descriptions.get(self.unwrap_locale_or_default(locale))
}
/// Attempts to get the rules in the specified locale, falls back to
/// `default_locale` if no rules were specified in this locale
pub fn get_rules(&self, locale: Option<&str>) -> Option<&str> {
self.descriptions
.get(self.unwrap_locale_or_default(locale))
.and_then(|d| d.rules.as_deref())
.or_else(|| {
self.descriptions
.get(&self.default_locale)?
.rules
.as_deref()
})
}
}
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,
})]),
}
}
/// Perform any needed validation on this server description that can't
/// be done using parsing.
///
/// The returned version being "Old" indicates the loaded setting has
/// 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
// for this version.
/* impl TryFrom<ServerDescription> for Final {
/*
use super::{v3 as next, MIGRATION_UPGRADE_GUARANTEE};
impl TryFrom<ServerDescription> for Final {
type Error = <Final as EditableSetting>::Error;
fn try_from(mut value: ServerDescription) -> Result<Final, Self::Error> {

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.is_empty() {
let localized_description = editable_settings
.server_description
.get(client.locale.as_deref());
if !localized_description.map_or(true, |d| d.motd.is_empty()) {
client.send(ServerGeneral::server_msg(
ChatType::CommandInfo,
editable_settings.server_description.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,13 @@ impl<'a> System<'a> for Sys {
// Tell the client its request was successful.
client.send(Ok(()))?;
let server_descriptions = &read_data.editable_settings.server_description;
let description = ServerDescription {
motd: server_descriptions.get(client.locale.as_deref()).map(|d| d.motd.clone()).unwrap_or_default(),
rules: server_descriptions.get_rules(client.locale.as_deref()).map(str::to_string),
};
// 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 +355,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,
send_to_server_checkbox,
send_to_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 send_to_server = ToggleButton::new(
self.global_state.settings.language.send_to_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.send_to_server_checkbox, ui);
if send_to_server != self.global_state.settings.language.send_to_server {
events.push(ToggleSendToServer(send_to_server));
}
Text::new(
&self
.localized_strings
.get_msg("hud-settings-language_send_to_server"),
)
.right_from(state.ids.send_to_server_checkbox, 10.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.graphics_for(state.ids.send_to_server_checkbox)
.color(TEXT_COLOR)
.set(state.ids.send_to_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.send_to_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,
@ -59,6 +60,8 @@ impl CharSelectionState {
})
.unwrap_or_default()
}
pub fn client(&self) -> &RefCell<Client> { &self.client }
}
impl PlayState for CharSelectionState {
@ -158,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

@ -1,8 +1,7 @@
mod client_init;
mod scene;
mod ui;
use super::char_selection::CharSelectionState;
use super::{char_selection::CharSelectionState, dummy_scene::Scene, server_info::ServerInfoState};
#[cfg(feature = "singleplayer")]
use crate::singleplayer::SingleplayerState;
use crate::{
@ -20,7 +19,6 @@ use client_init::{ClientInit, Error as InitError, Msg as InitMsg};
use common::comp;
use common_base::span;
use i18n::LocalizationHandle;
use scene::Scene;
#[cfg(feature = "singleplayer")]
use server::ServerInitStage;
use std::sync::Arc;
@ -28,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")]
@ -125,6 +125,9 @@ impl PlayState for MainMenuState {
ConnectionArgs::Mpsc(14004),
&mut self.init,
&global_state.tokio_runtime,
global_state.settings.language.send_to_server.then_some(
global_state.settings.language.selected_language.clone(),
),
&global_state.i18n,
);
},
@ -294,10 +297,27 @@ impl PlayState for MainMenuState {
core::mem::replace(&mut self.init, InitState::None)
{
self.main_menu_ui.connected();
return PlayStateResult::Push(Box::new(CharSelectionState::new(
let server_info = client.server_info().clone();
let server_description = client.server_description().clone();
let char_select = CharSelectionState::new(
global_state,
std::rc::Rc::new(std::cell::RefCell::new(*client)),
)));
);
let new_state = ServerInfoState::try_from_server_info(
global_state,
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 _);
return PlayStateResult::Push(new_state);
}
}
}
@ -342,6 +362,11 @@ impl PlayState for MainMenuState {
connection_args,
&mut self.init,
&global_state.tokio_runtime,
global_state
.settings
.language
.send_to_server
.then_some(global_state.settings.language.selected_language.clone()),
&global_state.i18n,
);
},
@ -572,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();
@ -604,6 +630,7 @@ fn attempt_login(
username,
password,
Arc::clone(runtime),
locale,
));
}
}

View File

@ -207,7 +207,7 @@ impl Showing {
}
}
struct Controls {
pub struct Controls {
fonts: Fonts,
imgs: Imgs,
bg_img: widget::image::Handle,
@ -677,6 +677,7 @@ pub struct MainMenuUi {
// TODO: re add this
// tip_no: u16,
controls: Controls,
bg_img_spec: &'static str,
}
impl MainMenuUi {
@ -695,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(
@ -707,9 +708,15 @@ impl MainMenuUi {
server,
);
Self { ui, controls }
Self {
ui,
controls,
bg_img_spec,
}
}
pub fn bg_img_spec(&self) -> &'static str { self.bg_img_spec }
pub fn update_language(&mut self, i18n: LocalizationHandle, settings: &Settings) {
self.controls.i18n = i18n;
let i18n = &i18n.read();
@ -807,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

@ -1,2 +1,4 @@
pub mod char_selection;
pub mod dummy_scene;
pub mod main;
pub mod server_info;

View File

@ -0,0 +1,372 @@
use super::{char_selection::CharSelectionState, dummy_scene::Scene};
use crate::{
render::{Drawer, GlobalsBindGroup},
settings::Settings,
ui::{
fonts::IcedFonts as Fonts,
ice::{component::neat_button, load_font, style, widget, Element, IcedUi as Ui},
img_ids::ImageGraphic,
Graphic,
},
window::{self, Event},
Direction, GlobalState, PlayState, PlayStateResult,
};
use client::ServerInfo;
use common::{
assets::{self, AssetExt},
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,
VerticalAlignment,
};
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
use tracing::error;
image_ids_ice! {
struct Imgs {
<ImageGraphic>
button: "voxygen.element.ui.generic.buttons.button",
button_hover: "voxygen.element.ui.generic.buttons.button_hover",
button_press: "voxygen.element.ui.generic.buttons.button_press",
}
}
pub struct Controls {
fonts: Fonts,
imgs: Imgs,
i18n: LocalizationHandle,
bg_img: widget::image::Handle,
accept_button: button::State,
decline_button: button::State,
scrollable: scrollable::State,
server_info: ServerInfo,
server_description: ServerDescription,
changed: bool,
}
pub struct ServerInfoState {
ui: Ui,
scene: Scene,
controls: Controls,
char_select: Option<CharSelectionState>,
}
#[derive(Clone)]
pub enum Message {
Accept,
Decline,
}
fn rules_hash(rules: &Option<String>) -> u64 {
let mut hasher = DefaultHasher::default();
rules.hash(&mut hasher);
hasher.finish()
}
impl ServerInfoState {
/// Create a new `MainMenuState`.
pub fn try_from_server_info(
global_state: &mut GlobalState,
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_description.rules.is_none()
|| server.map_or(false, |s| {
s.accepted_rules == Some(rules_hash(&server_description.rules))
}))
&& !force_show
{
return Err(char_select);
}
// Load language
let i18n = &global_state.i18n.read();
// TODO: don't add default font twice
let font = load_font(&i18n.fonts().get("cyri").unwrap().asset_key);
let mut ui = Ui::new(
&mut global_state.window,
font,
global_state.settings.interface.ui_scale,
)
.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 {
bg_img: ui.add_graphic(Graphic::Image(
assets::Image::load_expect(bg_img_spec).read().to_image(),
None,
)),
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,
accept_button: Default::default(),
decline_button: Default::default(),
scrollable: Default::default(),
server_info,
server_description,
changed,
},
ui,
char_select: Some(char_select),
})
}
fn handle_event(&mut self, event: window::Event) -> bool {
match event {
// Pass events to ui.
window::Event::IcedUi(event) => {
self.ui.handle_event(event);
true
},
window::Event::ScaleFactorChanged(s) => {
self.ui.scale_factor_changed(s);
false
},
_ => false,
}
}
}
impl PlayState for ServerInfoState {
fn enter(&mut self, _global_state: &mut GlobalState, _: Direction) {
/*
// Updated localization in case the selected language was changed
self.main_menu_ui
.update_language(global_state.i18n, &global_state.settings);
// Set scale mode in case it was change
self.main_menu_ui
.set_scale_mode(global_state.settings.interface.ui_scale);
*/
}
#[allow(clippy::single_match)] // TODO: remove when event match has multiple arms
fn tick(&mut self, global_state: &mut GlobalState, events: Vec<Event>) -> PlayStateResult {
span!(_guard, "tick", "<ServerInfoState as PlayState>::tick");
// Handle window events.
for event in events {
// Pass all events to the ui first.
if self.handle_event(event.clone()) {
continue;
}
match event {
Event::Close => return PlayStateResult::Shutdown,
// Ignore all other events.
_ => {},
}
}
if let Some(char_select) = &mut self.char_select {
if let Err(err) = char_select
.client()
.borrow_mut()
.tick(comp::ControllerInputs::default(), global_state.clock.dt())
{
let i18n = &global_state.i18n.read();
global_state.info_message =
Some(i18n.get_msg("common-connection_lost").into_owned());
error!(?err, "[server_info] Failed to tick the client");
return PlayStateResult::Pop;
}
}
// Maintain the UI.
let view = self.controls.view();
let (messages, _) = self.ui.maintain(
view,
global_state.window.renderer_mut(),
None,
&mut global_state.clipboard,
);
#[allow(clippy::never_loop)] // TODO: Remove when more message types are added
for message in messages {
match message {
Message::Accept => {
// Update last-accepted rules hash so we don't see the message again
if let Some(server) = global_state
.profile
.servers
.get_mut(&self.controls.server_info.name)
{
server.accepted_rules =
Some(rules_hash(&self.controls.server_description.rules));
}
return PlayStateResult::Switch(Box::new(self.char_select.take().unwrap()));
},
Message::Decline => return PlayStateResult::Pop,
}
}
PlayStateResult::Continue
}
fn name(&self) -> &'static str { "Server Info" }
fn capped_fps(&self) -> bool { true }
fn globals_bind_group(&self) -> &GlobalsBindGroup { self.scene.global_bind_group() }
fn render(&self, drawer: &mut Drawer<'_>, _: &Settings) {
// Draw the UI to the screen.
let mut third_pass = drawer.third_pass();
if let Some(mut ui_drawer) = third_pass.draw_ui() {
self.ui.render(&mut ui_drawer);
};
}
fn egui_enabled(&self) -> bool { false }
}
impl Controls {
fn view(&mut self) -> Element<Message> {
pub const TEXT_COLOR: iced::Color = iced::Color::from_rgb(1.0, 1.0, 1.0);
pub const IMPORTANT_TEXT_COLOR: iced::Color = iced::Color::from_rgb(1.0, 0.85, 0.5);
pub const DISABLED_TEXT_COLOR: iced::Color = iced::Color::from_rgba(1.0, 1.0, 1.0, 0.2);
pub const FILL_FRAC_ONE: f32 = 0.67;
let i18n = self.i18n.read();
// TODO: consider setting this as the default in the renderer
let button_style = style::button::Style::new(self.imgs.button)
.hover_image(self.imgs.button_hover)
.press_image(self.imgs.button_press)
.text_color(TEXT_COLOR)
.disabled_text_color(DISABLED_TEXT_COLOR);
let accept_button = Container::new(
Container::new(neat_button(
&mut self.accept_button,
i18n.get_msg("common-accept"),
FILL_FRAC_ONE,
button_style,
Some(Message::Accept),
))
.max_width(200),
)
.width(Length::Fill)
.align_x(Align::Center);
let decline_button = Container::new(
Container::new(neat_button(
&mut self.decline_button,
i18n.get_msg("common-decline"),
FILL_FRAC_ONE,
button_style,
Some(Message::Decline),
))
.max_width(200),
)
.width(Length::Fill)
.align_x(Align::Center);
let mut elements = Vec::new();
elements.push(
Container::new(
iced::Text::new(i18n.get_msg("main-server-rules"))
.size(self.fonts.cyri.scale(36))
.horizontal_alignment(HorizontalAlignment::Center),
)
.width(Length::Fill)
.into(),
);
if self.changed {
elements.push(
Container::new(
iced::Text::new(i18n.get_msg("main-server-rules-seen-before"))
.size(self.fonts.cyri.scale(30))
.color(IMPORTANT_TEXT_COLOR)
.horizontal_alignment(HorizontalAlignment::Center),
)
.width(Length::Fill)
.into(),
);
}
// elements.push(iced::Text::new(format!("{}: {}", self.server_info.name,
// self.server_info.description)) .size(self.fonts.cyri.scale(20))
// .width(Length::Shrink)
// .horizontal_alignment(HorizontalAlignment::Center)
// .into());
elements.push(
Scrollable::new(&mut self.scrollable)
.push(
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)
.into(),
);
elements.push(
Row::with_children(vec![decline_button.into(), accept_button.into()])
.width(Length::Shrink)
.height(Length::Shrink)
.padding(25)
.into(),
);
Container::new(
Container::new(
Column::with_children(elements)
.spacing(10)
.padding(20),
)
.style(
style::container::Style::color_with_double_cornerless_border(
(22, 18, 16, 255).into(),
(11, 11, 11, 255).into(),
(54, 46, 38, 255).into(),
),
)
.max_width(1000)
.align_x(Align::Center)
// .width(Length::Shrink)
// .height(Length::Shrink)
.padding(15),
)
.style(style::container::Style::image(self.bg_img))
.width(Length::Fill)
.height(Length::Fill)
.align_x(Align::Center)
.padding(50)
.into()
}
}

View File

@ -39,6 +39,8 @@ pub struct ServerProfile {
pub selected_character: Option<CharacterId>,
/// Last spectate position
pub spectate_position: Option<vek::Vec3<f32>>,
/// Hash of left-accepted server rules
pub accepted_rules: Option<u64>,
}
impl Default for ServerProfile {
@ -47,6 +49,7 @@ impl Default for ServerProfile {
characters: HashMap::new(),
selected_character: None,
spectate_position: None,
accepted_rules: None,
}
}
}

View File

@ -161,6 +161,7 @@ pub enum Interface {
#[derive(Clone)]
pub enum Language {
ChangeLanguage(Box<LanguageMetadata>),
ToggleSendToServer(bool),
ToggleEnglishFallback(bool),
}
#[derive(Clone)]
@ -717,6 +718,9 @@ impl SettingsChange {
.i18n
.set_english_fallback(settings.language.use_english_fallback);
},
Language::ToggleSendToServer(share) => {
settings.language.send_to_server = share;
},
},
SettingsChange::Networking(networking_change) => match networking_change {
Networking::AdjustTerrainViewDistance(terrain_vd) => {

View File

@ -4,6 +4,10 @@ use serde::{Deserialize, Serialize};
#[serde(default)]
pub struct LanguageSettings {
pub selected_language: String,
#[serde(default = "default_true")]
/// Controls whether the locale is sent to servers we connect (usually for
/// localizing rules & motd messages)
pub send_to_server: bool,
pub use_english_fallback: bool,
}
@ -11,7 +15,10 @@ impl Default for LanguageSettings {
fn default() -> Self {
Self {
selected_language: i18n::REFERENCE_LANG.to_string(),
send_to_server: true,
use_english_fallback: true,
}
}
}
fn default_true() -> bool { true }