mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
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:
commit
2a13fe3228
@ -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
|
||||
|
@ -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_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
|
||||
|
@ -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 }'.
|
||||
|
@ -63,6 +63,7 @@ fn main() {
|
||||
&mut None,
|
||||
&username,
|
||||
&password,
|
||||
None,
|
||||
|provider| provider == "https://auth.veloren.net",
|
||||
&|_| {},
|
||||
|_| {},
|
||||
|
@ -71,6 +71,7 @@ pub fn make_client(
|
||||
server_info,
|
||||
username,
|
||||
password,
|
||||
None,
|
||||
|_| true,
|
||||
&|_| {},
|
||||
|_| {},
|
||||
|
@ -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,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,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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).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())),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,6 +146,7 @@ impl ConnectionHandler {
|
||||
client_type,
|
||||
participant,
|
||||
server_data.time,
|
||||
None,
|
||||
general_stream,
|
||||
ping_stream,
|
||||
register_stream,
|
||||
|
@ -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(),
|
||||
|
@ -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
|
||||
|
@ -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> {
|
||||
|
@ -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),
|
||||
))?;
|
||||
}
|
||||
|
||||
|
@ -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.");
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
},
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -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() }
|
||||
|
@ -1,2 +1,4 @@
|
||||
pub mod char_selection;
|
||||
pub mod dummy_scene;
|
||||
pub mod main;
|
||||
pub mod server_info;
|
||||
|
372
voxygen/src/menu/server_info/mod.rs
Normal file
372
voxygen/src/menu/server_info/mod.rs
Normal 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()
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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 }
|
||||
|
Loading…
Reference in New Issue
Block a user