diff --git a/assets/voxygen/i18n/en/main.ftl b/assets/voxygen/i18n/en/main.ftl index bb137293ff..ad0bbb0ef3 100644 --- a/assets/voxygen/i18n/en/main.ftl +++ b/assets/voxygen/i18n/en/main.ftl @@ -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:' in crafting to see what recipes it's used in. - .a21 = You can take screenshots with '{ $gameinput-screenshot }'. \ No newline at end of file + .a21 = You can take screenshots with '{ $gameinput-screenshot }'. diff --git a/voxygen/src/menu/char_selection/mod.rs b/voxygen/src/menu/char_selection/mod.rs index 8fb6707683..8de9102655 100644 --- a/voxygen/src/menu/char_selection/mod.rs +++ b/voxygen/src/menu/char_selection/mod.rs @@ -59,6 +59,8 @@ impl CharSelectionState { }) .unwrap_or_default() } + + pub fn client(&self) -> &RefCell { &self.client } } impl PlayState for CharSelectionState { diff --git a/voxygen/src/menu/main/scene.rs b/voxygen/src/menu/dummy_scene.rs similarity index 100% rename from voxygen/src/menu/main/scene.rs rename to voxygen/src/menu/dummy_scene.rs diff --git a/voxygen/src/menu/main/mod.rs b/voxygen/src/menu/main/mod.rs index 0f6fe9d1c7..9d26fc2a51 100644 --- a/voxygen/src/menu/main/mod.rs +++ b/voxygen/src/menu/main/mod.rs @@ -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; @@ -294,10 +292,24 @@ 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 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, + ) + .map(|s| Box::new(s) as _) + .unwrap_or_else(|s| Box::new(s) as _); + + return PlayStateResult::Push(new_state); } } } diff --git a/voxygen/src/menu/main/ui/mod.rs b/voxygen/src/menu/main/ui/mod.rs index ff5181d575..13f5cb64b0 100644 --- a/voxygen/src/menu/main/ui/mod.rs +++ b/voxygen/src/menu/main/ui/mod.rs @@ -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 { @@ -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(); diff --git a/voxygen/src/menu/mod.rs b/voxygen/src/menu/mod.rs index 5e7dfc9cf6..3790b22796 100644 --- a/voxygen/src/menu/mod.rs +++ b/voxygen/src/menu/mod.rs @@ -1,2 +1,4 @@ pub mod char_selection; +pub mod dummy_scene; pub mod main; +pub mod server_info; diff --git a/voxygen/src/menu/server_info/mod.rs b/voxygen/src/menu/server_info/mod.rs new file mode 100644 index 0000000000..731983adfd --- /dev/null +++ b/voxygen/src/menu/server_info/mod.rs @@ -0,0 +1,336 @@ +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}; +use common_base::span; +use i18n::LocalizationHandle; +use iced::{ + button, scrollable, Align, Column, Container, HorizontalAlignment, Length, Row, Scrollable, + VerticalAlignment, +}; +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +image_ids_ice! { + struct Imgs { + + 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, + seen_before: bool, +} + +pub struct ServerInfoState { + ui: Ui, + scene: Scene, + controls: Controls, + char_select: Option, +} + +#[derive(Clone)] +pub enum Message { + Accept, + Decline, +} + +fn rules_hash(rules: &Option) -> 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, + ) -> Result { + let server = global_state.profile.servers.get(&server_info.name); + + // If there are no rules, or we've already accepted these rules, we don't need + // this state + if server_info.rules.is_none() + || server.map_or(false, |s| { + s.accepted_rules == Some(rules_hash(&server_info.rules)) + }) + { + 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(); + + 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.clone(), + accept_button: Default::default(), + decline_button: Default::default(), + scrollable: Default::default(), + server_info, + seen_before: server.map_or(false, |s| s.accepted_rules.is_some()), + }, + 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) -> PlayStateResult { + span!(_guard, "tick", "::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. + _ => {}, + } + } + + // Maintain the UI. + let view = self.controls.view(); + let (messages, _) = self.ui.maintain( + view, + global_state.window.renderer_mut(), + None, + &mut global_state.clipboard, + ); + + 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_info.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 { + 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(30)) + .horizontal_alignment(HorizontalAlignment::Center), + ) + .width(Length::Fill) + .into(), + ); + + if self.seen_before { + elements.push( + Container::new( + iced::Text::new(i18n.get_msg("main-server-rules-seen-before")) + .size(self.fonts.cyri.scale(20)) + .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_info.rules.as_deref().unwrap_or("")) + .size(self.fonts.cyri.scale(16)) + .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() + } +} diff --git a/voxygen/src/profile.rs b/voxygen/src/profile.rs index 765d8358fd..efc6ecb018 100644 --- a/voxygen/src/profile.rs +++ b/voxygen/src/profile.rs @@ -39,6 +39,8 @@ pub struct ServerProfile { pub selected_character: Option, /// Last spectate position pub spectate_position: Option>, + /// Hash of left-accepted server rules + pub accepted_rules: Option, } impl Default for ServerProfile { @@ -47,6 +49,7 @@ impl Default for ServerProfile { characters: HashMap::new(), selected_character: None, spectate_position: None, + accepted_rules: None, } } }