diff --git a/Cargo.lock b/Cargo.lock index 56f141905c..4446d54ec5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3244,6 +3244,7 @@ name = "veloren-voxygen" version = "0.4.0" dependencies = [ "backtrace 0.3.40 (registry+https://github.com/rust-lang/crates.io-index)", + "bincode 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", "conrod_core 0.63.0 (git+https://gitlab.com/veloren/conrod.git)", "conrod_winit 0.63.0 (git+https://gitlab.com/veloren/conrod.git)", diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index 1760929b2f..aab878573f 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -62,9 +62,10 @@ crossbeam = "=0.7.2" hashbrown = { version = "0.6.2", features = ["serde", "nightly"] } chrono = "0.4.9" rust-argon2 = "0.5" +bincode = "1.2" [target.'cfg(target_os = "macos")'.dependencies] -dispatch = "0.1.4" +dispatch = "0.1.4" [target.'cfg(windows)'.build-dependencies] winres = "0.1" diff --git a/voxygen/src/main.rs b/voxygen/src/main.rs index 5c5b3c5ff8..d156e89bbe 100644 --- a/voxygen/src/main.rs +++ b/voxygen/src/main.rs @@ -13,6 +13,7 @@ pub mod key_state; mod logging; pub mod menu; pub mod mesh; +pub mod meta; pub mod render; pub mod scene; pub mod session; @@ -24,13 +25,16 @@ pub mod window; // Reexports pub use crate::error::Error; -use crate::{audio::AudioFrontend, menu::main::MainMenuState, settings::Settings, window::Window}; +use crate::{ + audio::AudioFrontend, menu::main::MainMenuState, meta::Meta, settings::Settings, window::Window, +}; use log::{debug, error}; use std::{mem, panic, str::FromStr}; /// A type used to store state that is shared between all play states. pub struct GlobalState { settings: Settings, + meta: Meta, window: Window, audio: AudioFrontend, info_message: Option, @@ -97,6 +101,9 @@ fn main() { logging::init(&settings, term_log_level, file_log_level); + // Load metadata + let meta = Meta::load(); + // Save settings to add new fields or create the file if it is not already there if let Err(err) = settings.save_to_file() { panic!("Failed to save settings: {:?}", err); @@ -120,6 +127,7 @@ fn main() { audio, window: Window::new(&settings).expect("Failed to create window!"), settings, + meta, info_message: None, }; @@ -254,6 +262,7 @@ fn main() { } } - // Save any unsaved changes to settings + // Save any unsaved changes to settings and meta global_state.settings.save_to_file_warn(); + global_state.meta.save_to_file_warn(); } diff --git a/voxygen/src/menu/char_selection/mod.rs b/voxygen/src/menu/char_selection/mod.rs index 62af6386e9..10b3b6ed5f 100644 --- a/voxygen/src/menu/char_selection/mod.rs +++ b/voxygen/src/menu/char_selection/mod.rs @@ -57,7 +57,7 @@ impl PlayState for CharSelectionState { // Maintain the UI. let events = self .char_selection_ui - .maintain(global_state.window.renderer_mut(), &self.client.borrow()); + .maintain(global_state, &self.client.borrow()); for event in events { match event { ui::Event::Logout => { diff --git a/voxygen/src/menu/char_selection/ui.rs b/voxygen/src/menu/char_selection/ui.rs index a0bab3aa5b..c37016b2dc 100644 --- a/voxygen/src/menu/char_selection/ui.rs +++ b/voxygen/src/menu/char_selection/ui.rs @@ -1,5 +1,6 @@ use crate::window::{Event as WinEvent, PressState}; use crate::{ + meta::CharacterData, render::{Consts, Globals, Renderer}, ui::{ img_ids::{BlankGraphic, ImageGraphic, VoxelGraphic, VoxelMs9Graphic}, @@ -8,7 +9,7 @@ use crate::{ GlobalState, }; use client::Client; -use common::comp::humanoid; +use common::comp::{self, humanoid}; use conrod_core::{ color, color::TRANSPARENT, @@ -43,7 +44,6 @@ widget_ids! { divider, bodyrace_text, facialfeatures_text, - char_delete, info_bg, info_frame, info_button_align, @@ -61,10 +61,11 @@ widget_ids! { // Characters - character_box_1, - character_name_1, - character_location_1, - character_level_1, + character_boxes[], + character_deletes[], + character_names[], + character_locations[], + character_levels[], character_box_2, character_name_2, @@ -242,7 +243,8 @@ const TEXT_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0); const TEXT_COLOR_2: Color = Color::Rgba(1.0, 1.0, 1.0, 0.2); enum InfoContent { - Deletion, + None, + Deletion(usize), //Name, } @@ -254,7 +256,6 @@ pub struct CharSelectionUi { fonts: Fonts, character_creation: bool, info_content: InfoContent, - info_window: bool, //deletion_confirmation: bool, pub character_name: String, pub character_body: humanoid::Body, @@ -283,18 +284,29 @@ impl CharSelectionUi { imgs, rot_imgs, fonts, - info_window: false, - info_content: InfoContent::Deletion, + info_content: InfoContent::None, //deletion_confirmation: false, character_creation: false, character_name: "Character Name".to_string(), - character_body: humanoid::Body::random(), + character_body: if let Some(character) = global_state + .meta + .characters + .get(global_state.meta.selected_character) + { + match character.body { + comp::Body::Humanoid(body) => Some(body.clone()), + _ => None, + } + } else { + None + } + .unwrap_or_else(|| humanoid::Body::random()), character_tool: Some(STARTER_SWORD), } } // TODO: Split this into multiple modules or functions. - fn update_layout(&mut self, client: &Client) -> Vec { + fn update_layout(&mut self, global_state: &mut GlobalState, client: &Client) -> Vec { let mut events = Vec::new(); let (ref mut ui_widgets, ref mut tooltip_manager) = self.ui.set_widgets(); let version = format!( @@ -322,7 +334,8 @@ impl CharSelectionUi { .desc_text_color(TEXT_COLOR_2); // Information Window - if self.info_window { + if let InfoContent::None = self.info_content { + } else { Rectangle::fill_with([520.0, 150.0], color::rgba(0.0, 0.0, 0.0, 0.9)) .mid_top_with_margin_on(ui_widgets.window, 300.0) .set(self.ids.info_bg, ui_widgets); @@ -334,7 +347,8 @@ impl CharSelectionUi { .bottom_left_with_margins_on(self.ids.info_frame, 0.0, 0.0) .set(self.ids.info_button_align, ui_widgets); match self.info_content { - InfoContent::Deletion => { + InfoContent::None => unreachable!(), + InfoContent::Deletion(character_index) => { Text::new("Permanently delete this Character?") .mid_top_with_margin_on(self.ids.info_frame, 40.0) .font_size(24) @@ -354,23 +368,23 @@ impl CharSelectionUi { .set(self.ids.info_no, ui_widgets) .was_clicked() { - self.info_window = false + self.info_content = InfoContent::None; }; if Button::image(self.imgs.button) .w_h(150.0, 40.0) .right_from(self.ids.info_no, 100.0) - //.hover_image(self.imgs.button_hover) - //.press_image(self.imgs.button_press) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) .label_y(Relative::Scalar(2.0)) .label("Yes") - .label_font_size(18) .label_font_id(self.fonts.cyri) - .label_color(Color::Rgba(1.0, 1.0, 1.0, 0.1)) + .label_font_size(18) + .label_color(TEXT_COLOR) .set(self.ids.info_ok, ui_widgets) .was_clicked() { - //self.info_window = false - // TODO -> Char Deletion Event + self.info_content = InfoContent::None; + global_state.meta.delete_character(character_index); }; } } @@ -489,54 +503,96 @@ impl CharSelectionUi { .color(TEXT_COLOR) .set(self.ids.version, ui_widgets); - // 1st Character in Selection List - if Button::image(self.imgs.selection) - .top_left_with_margins_on(self.ids.charlist_alignment, 0.0, 2.0) - .w_h(386.0, 80.0) - .image_color(Color::Rgba(1.0, 1.0, 1.0, 0.8)) - .hover_image(self.imgs.selection) - .press_image(self.imgs.selection) - .label_font_id(self.fonts.cyri) - .label_y(conrod_core::position::Relative::Scalar(20.0)) - .set(self.ids.character_box_1, ui_widgets) - .was_clicked() - {} - if Button::image(self.imgs.delete_button) - .w_h(30.0 * 0.5, 30.0 * 0.5) - .top_right_with_margins_on(self.ids.character_box_1, 15.0, 15.0) - .hover_image(self.imgs.delete_button_hover) - .press_image(self.imgs.delete_button_press) - .with_tooltip(tooltip_manager, "Delete Character", "", &tooltip_human) - .set(self.ids.char_delete, ui_widgets) - .was_clicked() - { - self.info_content = InfoContent::Deletion; - self.info_window = true; + // Resize character selection widgets + let character_count = global_state.meta.characters.len(); + self.ids + .character_boxes + .resize(character_count, &mut ui_widgets.widget_id_generator()); + self.ids + .character_deletes + .resize(character_count, &mut ui_widgets.widget_id_generator()); + self.ids + .character_names + .resize(character_count, &mut ui_widgets.widget_id_generator()); + self.ids + .character_levels + .resize(character_count, &mut ui_widgets.widget_id_generator()); + self.ids + .character_locations + .resize(character_count, &mut ui_widgets.widget_id_generator()); + + // Character selection + for (i, character) in global_state.meta.characters.iter().enumerate() { + let character_box = Button::image(if global_state.meta.selected_character == i { + self.imgs.selection_hover + } else { + self.imgs.selection + }); + let character_box = if i == 0 { + character_box.top_left_with_margins_on(self.ids.charlist_alignment, 0.0, 2.0) + } else { + character_box.down_from(self.ids.character_boxes[i - 1], 5.0) + }; + if character_box + .w_h(386.0, 80.0) + .image_color(Color::Rgba(1.0, 1.0, 1.0, 0.8)) + .hover_image(self.imgs.selection_hover) + .press_image(self.imgs.selection_press) + .label_font_id(self.fonts.cyri) + .label_y(conrod_core::position::Relative::Scalar(20.0)) + .set(self.ids.character_boxes[i], ui_widgets) + .was_clicked() + { + self.character_name = character.name.clone(); + self.character_body = match &character.body { + comp::Body::Humanoid(body) => body.clone(), + _ => panic!("Unsupported body type!"), + }; + global_state.meta.selected_character = i; + } + if Button::image(self.imgs.delete_button) + .w_h(30.0 * 0.5, 30.0 * 0.5) + .top_right_with_margins_on(self.ids.character_boxes[i], 15.0, 15.0) + .hover_image(self.imgs.delete_button_hover) + .press_image(self.imgs.delete_button_press) + .with_tooltip(tooltip_manager, "Delete Character", "", &tooltip_human) + .set(self.ids.character_deletes[i], ui_widgets) + .was_clicked() + { + self.info_content = InfoContent::Deletion(i); + } + Text::new(&character.name) + .top_left_with_margins_on(self.ids.character_boxes[i], 6.0, 9.0) + .font_size(19) + .font_id(self.fonts.cyri) + .color(TEXT_COLOR) + .set(self.ids.character_names[i], ui_widgets); + + Text::new("Level ") + .down_from(self.ids.character_names[i], 4.0) + .font_size(17) + .font_id(self.fonts.cyri) + .color(TEXT_COLOR) + .set(self.ids.character_levels[i], ui_widgets); + + Text::new("Uncanny Valley") + .down_from(self.ids.character_levels[i], 4.0) + .font_size(17) + .font_id(self.fonts.cyri) + .color(TEXT_COLOR) + .set(self.ids.character_locations[i], ui_widgets); } - Text::new("Test Character") - .top_left_with_margins_on(self.ids.character_box_1, 6.0, 9.0) - .font_size(19) - .font_id(self.fonts.cyri) - .color(TEXT_COLOR) - .set(self.ids.character_name_1, ui_widgets); - - Text::new("Level 1") - .down_from(self.ids.character_name_1, 4.0) - .font_size(17) - .font_id(self.fonts.cyri) - .color(TEXT_COLOR) - .set(self.ids.character_level_1, ui_widgets); - - Text::new("Uncanny Valley") - .down_from(self.ids.character_level_1, 4.0) - .font_size(17) - .font_id(self.fonts.cyri) - .color(TEXT_COLOR) - .set(self.ids.character_location_1, ui_widgets); // Create Character Button - if Button::image(self.imgs.selection) - .down_from(self.ids.character_box_1, 5.0) + let create_char_button = Button::image(self.imgs.selection); + + let create_char_button = if character_count > 0 { + create_char_button.down_from(self.ids.character_boxes[character_count - 1], 5.0) + } else { + create_char_button.top_left_with_margins_on(self.ids.charlist_alignment, 0.0, 2.0) + }; + + if create_char_button .w_h(386.0, 80.0) .hover_image(self.imgs.selection_hover) .press_image(self.imgs.selection_press) @@ -549,6 +605,7 @@ impl CharSelectionUi { { self.character_creation = true; self.character_tool = Some(STARTER_SWORD); + self.character_body = humanoid::Body::random(); } } // Character_Creation ////////////////////////////////////////////////////////////////////// @@ -585,6 +642,10 @@ impl CharSelectionUi { { // TODO: Save character. self.character_creation = false; + global_state.meta.add_character(CharacterData { + name: self.character_name.clone(), + body: comp::Body::Humanoid(self.character_body.clone()), + }); } // Character Name Input Rectangle::fill_with([320.0, 50.0], color::rgba(0.0, 0.0, 0.0, 0.97)) @@ -1186,9 +1247,9 @@ impl CharSelectionUi { } } - pub fn maintain(&mut self, renderer: &mut Renderer, client: &Client) -> Vec { - let events = self.update_layout(client); - self.ui.maintain(renderer, None); + pub fn maintain(&mut self, global_state: &mut GlobalState, client: &Client) -> Vec { + let events = self.update_layout(global_state, client); + self.ui.maintain(global_state.window.renderer_mut(), None); events } diff --git a/voxygen/src/meta.rs b/voxygen/src/meta.rs new file mode 100644 index 0000000000..36a0f10971 --- /dev/null +++ b/voxygen/src/meta.rs @@ -0,0 +1,87 @@ +use common::comp; +use directories::ProjectDirs; +use log::warn; +use serde_derive::{Deserialize, Serialize}; +use std::{fs, io, path::PathBuf}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CharacterData { + pub name: String, + pub body: comp::Body, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct Meta { + pub characters: Vec, + pub selected_character: usize, +} + +impl Meta { + pub fn delete_character(&mut self, index: usize) { + self.characters.remove(index); + if index < self.selected_character { + self.selected_character -= 1; + } + } + + pub fn add_character(&mut self, data: CharacterData) { + self.characters.push(data); + } + + pub fn load() -> Self { + let path = Self::get_meta_path(); + + if let Ok(file) = fs::File::open(&path) { + match bincode::deserialize_from(file) { + Ok(s) => return s, + Err(e) => { + log::warn!("Failed to parse meta file! Fallback to default. {}", e); + // Rename the corrupted settings file + let mut new_path = path.to_owned(); + new_path.pop(); + new_path.push("meta.invalid.dat"); + if let Err(err) = std::fs::rename(path, new_path) { + log::warn!("Failed to rename meta file. {}", err); + } + } + } + } + // This is reached if either: + // - The file can't be opened (presumably it doesn't exist) + // - Or there was an error parsing the file + let default = Self::default(); + default.save_to_file_warn(); + default + } + + pub fn save_to_file_warn(&self) { + if let Err(err) = self.save_to_file() { + warn!("Failed to save settings: {:?}", err); + } + } + + pub fn save_to_file(&self) -> std::io::Result<()> { + let path = Self::get_meta_path(); + if let Some(dir) = path.parent() { + fs::create_dir_all(dir)?; + } + bincode::serialize_into(fs::File::create(path)?, self) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + Ok(()) + } + + pub fn get_meta_path() -> PathBuf { + if let Some(val) = std::env::var_os("VOXYGEN_CONFIG") { + let meta = PathBuf::from(val).join("meta.dat"); + if meta.exists() || meta.parent().map(|x| x.exists()).unwrap_or(false) { + return meta; + } + log::warn!("VOXYGEN_CONFIG points to invalid path."); + } + + let proj_dirs = ProjectDirs::from("net", "veloren", "voxygen") + .expect("System's $HOME directory path not found!"); + proj_dirs.config_dir().join("meta").with_extension("dat") + } +}