From 890c0596ed8f1578a5df304e53d4ba05b4565f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Sallai?= Date: Thu, 23 Feb 2023 14:27:53 +0200 Subject: [PATCH] add portable user directory setting --- src/data/builtin/builtin_data/locale/en.json | 9 +- src/data/builtin/builtin_data/locale/jp.json | 9 +- src/framework/filesystem.rs | 28 ++++- src/framework/vfs.rs | 5 + src/game/filesystem_container.rs | 50 ++++++-- src/menu/controls_menu.rs | 16 ++- src/menu/coop_menu.rs | 4 +- src/menu/mod.rs | 124 +++++++++++++++++-- src/menu/pause_menu.rs | 4 +- src/menu/save_select_menu.rs | 10 +- src/menu/settings_menu.rs | 107 ++++++++++++++-- src/scene/title_scene.rs | 6 +- 12 files changed, 319 insertions(+), 53 deletions(-) diff --git a/src/data/builtin/builtin_data/locale/en.json b/src/data/builtin/builtin_data/locale/en.json index 17961b6..f668aa2 100644 --- a/src/data/builtin/builtin_data/locale/en.json +++ b/src/data/builtin/builtin_data/locale/en.json @@ -133,7 +133,14 @@ "advanced": "Advanced...", "advanced_menu": { "open_user_data": "Open user data directory", - "open_game_data": "Open game data directory" + "open_game_data": "Open game data directory", + "make_portable": "Make portable user directory" + }, + "portable_menu": { + "explanation": "This will create a local user data directory and copy your settings and save files.", + "restart_question": "Reload the game to use the new location?", + "restart": "Save and return to title", + "cancel": "Cancel" } }, "controls_menu": { diff --git a/src/data/builtin/builtin_data/locale/jp.json b/src/data/builtin/builtin_data/locale/jp.json index 7e263b4..dcc147f 100644 --- a/src/data/builtin/builtin_data/locale/jp.json +++ b/src/data/builtin/builtin_data/locale/jp.json @@ -133,7 +133,14 @@ "advanced": "詳細設定", "advanced_menu": { "open_user_data": "ユーザープロファイルを開く", - "open_game_data": "ゲームファイルを開く" + "open_game_data": "ゲームファイルを開く", + "make_portable": "ポータブルユーザーディレクトリを作成する" + }, + "portable_menu": { + "explanation": "ローカルのユーザーデータディレクトリが作成され、設定とセーブファイルがそこにコピーされます。", + "restart_question": "新しい場所を使うには、ゲームを再起動しますか?", + "restart": "保存してタイトルに戻る", + "cancel": "キャンセル" } }, "controls_menu": { diff --git a/src/framework/filesystem.rs b/src/framework/filesystem.rs index f2fd53b..6bdead6 100644 --- a/src/framework/filesystem.rs +++ b/src/framework/filesystem.rs @@ -160,7 +160,7 @@ impl Filesystem { pub(crate) fn user_read_dir>( &self, path: P, - ) -> GameResult>> { + ) -> GameResult>> { let itr = self .user_vfs .read_dir(path.as_ref())? @@ -175,7 +175,7 @@ impl Filesystem { pub(crate) fn read_dir>( &self, path: P, - ) -> GameResult>> { + ) -> GameResult>> { let itr = self .vfs .read_dir(path.as_ref())? @@ -221,6 +221,14 @@ impl Filesystem { pub fn mount_user_vfs(&mut self, vfs: Box) { self.user_vfs.push_back(vfs); } + + pub fn unmount_vfs(&mut self, root: &PathBuf) { + self.vfs.remove(root); + } + + pub fn unmount_user_vfs(&mut self, root: &PathBuf) { + self.user_vfs.remove(root); + } } /// Opens the given path and returns the resulting `File` @@ -303,7 +311,7 @@ pub fn user_is_dir>(ctx: &Context, path: P) -> bool { pub fn user_read_dir>( ctx: &Context, path: P, -) -> GameResult>> { +) -> GameResult>> { ctx.filesystem.user_read_dir(path) } @@ -339,7 +347,7 @@ pub fn is_dir>(ctx: &Context, path: P) -> bool { /// in no particular order. /// /// Lists the base directory if an empty path is given. -pub fn read_dir>(ctx: &Context, path: P) -> GameResult>> { +pub fn read_dir>(ctx: &Context, path: P) -> GameResult>> { ctx.filesystem.read_dir(path) } @@ -347,7 +355,7 @@ pub fn read_dir_find>( ctx: &Context, roots: &Vec, path: P, -) -> GameResult>> { +) -> GameResult>> { let mut files = Vec::new(); for root in roots { @@ -383,3 +391,13 @@ pub fn mount_vfs(ctx: &mut Context, vfs: Box) { pub fn mount_user_vfs(ctx: &mut Context, vfs: Box) { ctx.filesystem.mount_user_vfs(vfs) } + +/// Unmounts a VFS with a provided root path. +pub fn unmount_vfs(ctx: &mut Context, root: &PathBuf) { + ctx.filesystem.unmount_vfs(root) +} + +/// Unmounts a user VFS with a provided root path. +pub fn unmount_user_vfs(ctx: &mut Context, root: &PathBuf) { + ctx.filesystem.unmount_user_vfs(root) +} diff --git a/src/framework/vfs.rs b/src/framework/vfs.rs index efe9f2f..5cd0517 100644 --- a/src/framework/vfs.rs +++ b/src/framework/vfs.rs @@ -441,6 +441,11 @@ impl OverlayFS { pub fn roots(&self) -> &VecDeque> { &self.roots } + + /// Removes a VFS with a provided root. + pub fn remove(&mut self, root: &PathBuf) { + self.roots.iter().position(|fs| fs.to_path_buf() == Some(root.clone())).map(|i| self.roots.remove(i)); + } } impl VFS for OverlayFS { diff --git a/src/game/filesystem_container.rs b/src/game/filesystem_container.rs index 81a9998..bd6776d 100644 --- a/src/game/filesystem_container.rs +++ b/src/game/filesystem_container.rs @@ -5,7 +5,7 @@ use crate::{ framework::{ context::Context, error::GameResult, - filesystem::{mount_user_vfs, mount_vfs}, + filesystem::{mount_user_vfs, mount_vfs, unmount_user_vfs}, vfs::PhysicalFS, }, }; @@ -13,11 +13,13 @@ use crate::{ pub struct FilesystemContainer { pub user_path: PathBuf, pub game_path: PathBuf, + + pub is_portable: bool, } impl FilesystemContainer { pub fn new() -> Self { - Self { user_path: PathBuf::new(), game_path: PathBuf::new() } + Self { user_path: PathBuf::new(), game_path: PathBuf::new(), is_portable: false } } pub fn mount_fs(&mut self, context: &mut Context) -> GameResult { @@ -125,14 +127,15 @@ impl FilesystemContainer { #[cfg(not(any(target_os = "android", target_os = "horizon")))] { - if crate::framework::filesystem::open(&context, "/.drs_localstorage").is_ok() { - let mut user_dir = resource_dir.clone(); - user_dir.push("_drs_profile"); + let mut user_dir = resource_dir.clone(); + user_dir.pop(); + user_dir.push("user"); - let _ = std::fs::create_dir_all(&user_dir); + if user_dir.is_dir() { + // portable mode mount_user_vfs(context, Box::new(PhysicalFS::new(&user_dir, false))); - self.user_path = user_dir.clone(); + self.is_portable = true; } else { let user_dir = project_dirs.data_local_dir(); mount_user_vfs(context, Box::new(PhysicalFS::new(user_dir, false))); @@ -155,6 +158,39 @@ impl FilesystemContainer { self.open_directory(self.game_path.clone()) } + pub fn make_portable_user_directory(&mut self, ctx: &mut Context) -> GameResult { + let mut user_dir = self.game_path.clone(); + user_dir.pop(); + user_dir.push("user"); + + if user_dir.is_dir() { + return Ok(()); // portable directory already exists + } + + let _ = std::fs::create_dir_all(user_dir.clone()); + + // copy user data from current user dir + for entry in std::fs::read_dir(&self.user_path)? { + let entry = entry?; + let path = entry.path(); + let file_name = path.file_name().unwrap().to_str().unwrap(); + let mut new_path = user_dir.clone(); + new_path.push(file_name); + std::fs::copy(path, new_path)?; + } + + // unmount old user dir + unmount_user_vfs(ctx, &self.user_path); + + // mount new user dir + mount_user_vfs(ctx, Box::new(PhysicalFS::new(&user_dir, false))); + + self.user_path = user_dir.clone(); + self.is_portable = true; + + Ok(()) + } + fn open_directory(&self, path: PathBuf) -> GameResult { #[cfg(target_os = "horizon")] return Ok(()); // can't open directories on switch diff --git a/src/menu/controls_menu.rs b/src/menu/controls_menu.rs index 4d066d1..b6b15ac 100644 --- a/src/menu/controls_menu.rs +++ b/src/menu/controls_menu.rs @@ -211,10 +211,8 @@ impl ControlsMenu { MainMenuEntry::Controller, MenuEntry::Active(state.loc.t("menus.controls_menu.controller.entry").to_owned()), ); - self.main.push_entry( - MainMenuEntry::Rebind, - MenuEntry::Active(state.loc.t("menus.controls_menu.rebind").to_owned()), - ); + self.main + .push_entry(MainMenuEntry::Rebind, MenuEntry::Active(state.loc.t("menus.controls_menu.rebind").to_owned())); self.main.push_entry(MainMenuEntry::Rumble, MenuEntry::Hidden); self.main.push_entry(MainMenuEntry::Back, MenuEntry::Active(state.loc.t("common.back").to_owned())); @@ -246,28 +244,28 @@ impl ControlsMenu { fn update_sizes(&mut self, state: &SharedGameState) { self.main.update_width(state); - self.main.update_height(); + self.main.update_height(state); self.main.x = ((state.canvas_size.0 - self.main.width as f32) / 2.0).floor() as isize; self.main.y = ((state.canvas_size.1 - self.main.height as f32) / 2.0).floor() as isize; self.select_controller.update_width(state); - self.select_controller.update_height(); + self.select_controller.update_height(state); self.select_controller.x = ((state.canvas_size.0 - self.select_controller.width as f32) / 2.0).floor() as isize; self.select_controller.y = ((state.canvas_size.1 - self.select_controller.height as f32) / 2.0).floor() as isize; self.rebind.update_width(state); - self.rebind.update_height(); + self.rebind.update_height(state); self.rebind.x = ((state.canvas_size.0 - self.rebind.width as f32) / 2.0).floor() as isize; self.rebind.y = ((state.canvas_size.1 - self.rebind.height as f32) / 2.0).floor() as isize; self.confirm_rebind.update_width(state); - self.confirm_rebind.update_height(); + self.confirm_rebind.update_height(state); self.confirm_rebind.x = ((state.canvas_size.0 - self.confirm_rebind.width as f32) / 2.0).floor() as isize; self.confirm_rebind.y = ((state.canvas_size.1 - self.confirm_rebind.height as f32) / 2.0).floor() as isize; self.confirm_reset.update_width(state); - self.confirm_reset.update_height(); + self.confirm_reset.update_height(state); self.confirm_reset.x = ((state.canvas_size.0 - self.confirm_reset.width as f32) / 2.0).floor() as isize; self.confirm_reset.y = ((state.canvas_size.1 - self.confirm_reset.height as f32) / 2.0).floor() as isize; } diff --git a/src/menu/coop_menu.rs b/src/menu/coop_menu.rs index 3565408..2c521ce 100644 --- a/src/menu/coop_menu.rs +++ b/src/menu/coop_menu.rs @@ -96,12 +96,12 @@ impl PlayerCountMenu { fn update_sizes(&mut self, state: &SharedGameState) { self.coop_menu.update_width(state); - self.coop_menu.update_height(); + self.coop_menu.update_height(state); self.coop_menu.x = ((state.canvas_size.0 - self.coop_menu.width as f32) / 2.0).floor() as isize; self.coop_menu.y = 30 + ((state.canvas_size.1 - self.coop_menu.height as f32) / 2.0).floor() as isize; self.skin_menu.update_width(state); - self.skin_menu.update_height(); + self.skin_menu.update_height(state); self.skin_menu.x = ((state.canvas_size.0 - self.coop_menu.width as f32) / 2.0).floor() as isize; self.skin_menu.y = 30 + ((state.canvas_size.1 - self.coop_menu.height as f32) / 2.0).floor() as isize; } diff --git a/src/menu/mod.rs b/src/menu/mod.rs index 18264e4..faf0c26 100644 --- a/src/menu/mod.rs +++ b/src/menu/mod.rs @@ -28,6 +28,8 @@ pub enum ControlMenuData { #[derive(Clone)] pub enum MenuEntry { Hidden, + Title(String, bool, bool), // text, centered, white + LongText(String, bool, bool), // text, centered, white Active(String), DisabledWhite(String), Disabled(String), @@ -40,12 +42,15 @@ pub enum MenuEntry { NewSave, PlayerSkin, Control(String, ControlMenuData), + Spacer(f64), } impl MenuEntry { pub fn height(&self) -> f64 { match self { MenuEntry::Hidden => 0.0, + MenuEntry::Title(_, _, _) => 16.0, // individual line + MenuEntry::LongText(_, _, _) => 16.0, // individual line MenuEntry::Active(_) => 16.0, MenuEntry::DisabledWhite(_) => 16.0, MenuEntry::Disabled(_) => 16.0, @@ -58,12 +63,15 @@ impl MenuEntry { MenuEntry::NewSave => 32.0, MenuEntry::PlayerSkin => 24.0, MenuEntry::Control(_, _) => 16.0, + MenuEntry::Spacer(height) => *height, } } pub fn selectable(&self) -> bool { match self { MenuEntry::Hidden => false, + MenuEntry::Title(_, _, _) => false, + MenuEntry::LongText(_, _, _) => false, MenuEntry::Active(_) => true, MenuEntry::DisabledWhite(_) => false, MenuEntry::Disabled(_) => false, @@ -76,6 +84,7 @@ impl MenuEntry { MenuEntry::NewSave => true, MenuEntry::PlayerSkin => true, MenuEntry::Control(_, _) => true, + MenuEntry::Spacer(_) => false, } } } @@ -95,11 +104,13 @@ pub struct Menu { pub height: u16, pub selected: T, pub entries: Vec<(T, MenuEntry)>, + pub height_overrides: Vec<(T, f64)>, anim_num: u16, anim_wait: u16, custom_cursor: Cell, pub draw_cursor: bool, pub non_interactive: bool, + pub center_options: bool, } impl Menu { @@ -113,9 +124,11 @@ impl Menu { anim_num: 0, anim_wait: 0, entries: Vec::new(), + height_overrides: Vec::new(), custom_cursor: Cell::new(true), draw_cursor: true, non_interactive: false, + center_options: false, } } @@ -151,6 +164,10 @@ impl Menu { let entry_width = state.font.builder().compute_width(&entry) + 32.0; width = width.max(entry_width); } + MenuEntry::Title(entry, _, _) | MenuEntry::LongText(entry, _, _) => { + let entry_width = state.font.builder().compute_width(&entry).min(state.canvas_size.0) + 32.0; + width = width.max(entry_width); + } MenuEntry::Toggle(entry, _) => { let mut entry_with_option = entry.clone(); entry_with_option.push_str(" "); @@ -199,18 +216,33 @@ impl Menu { MenuEntry::NewSave => {} MenuEntry::PlayerSkin => {} MenuEntry::Control(_, _) => {} + MenuEntry::Spacer(_) => {} } } - width = width.max(16.0); + width = width.max(16.0).min(state.canvas_size.0 - MENU_MIN_PADDING); self.width = if (width + 4.0) % 8.0 != 0.0 { (width + 4.0 - width % 8.0) as u16 } else { width as u16 }; } - pub fn update_height(&mut self) { + pub fn update_height(&mut self, state: &SharedGameState) { let mut height = 8.0; - for (_, entry) in &self.entries { - height += entry.height(); + for (id, entry) in &self.entries { + match entry { + MenuEntry::Title(text, _, _) | MenuEntry::LongText(text, _, _) => { + let text_width = state.font.builder().compute_width(text) + 32.0; + let lines = (text_width / state.canvas_size.0).ceil(); + + let actual_entry_height = lines as f64 * entry.height(); + + self.height_overrides.push((id.clone(), actual_entry_height)); + + height += actual_entry_height; + } + _ => { + height += entry.height(); + } + } } self.height = height.max(16.0) as u16; @@ -320,6 +352,26 @@ impl Menu { batch.draw(ctx)?; + let options_x = if self.center_options { + let mut longest_option_width = 20.0; + + for (_, entry) in &self.entries { + match entry { + MenuEntry::Options(text, _, _) | MenuEntry::Active(text) => { + let text_width = state.font.builder().compute_width(text) + 32.0; + if text_width > longest_option_width { + longest_option_width = text_width; + } + } + _ => {} + } + } + + (state.canvas_size.0 / 2.0) - (longest_option_width / 2.0) + } else { + self.x as f32 + }; + if self.draw_cursor { if self.custom_cursor.get() { if let Ok(batch) = state.texture_set.get_or_load_batch(ctx, &state.constants, "MenuCursor") { @@ -328,7 +380,7 @@ impl Menu { rect.right = rect.left + 16; rect.bottom = rect.top + 16; - batch.add_rect(self.x as f32, computed_y + 3.0 + selected_y, &rect); + batch.add_rect(options_x, computed_y + 3.0 + selected_y, &rect); batch.draw(ctx)?; } else { @@ -365,7 +417,7 @@ impl Menu { let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, menu_texture)?; - batch.add_rect(self.x as f32, computed_y + 4.0 + selected_y, &character_rect[self.anim_num as usize]); + batch.add_rect(options_x, computed_y + 4.0 + selected_y, &character_rect[self.anim_num as usize]); batch.draw(ctx)?; } @@ -375,13 +427,60 @@ impl Menu { for (_, entry) in &self.entries { match entry { MenuEntry::Active(name) | MenuEntry::DisabledWhite(name) => { - state.font.builder().position(self.x as f32 + 20.0, y).draw( + state.font.builder().position(options_x + 20.0, y).draw( name, ctx, &state.constants, &mut state.texture_set, )?; } + MenuEntry::Title(text, is_centered, is_white) | MenuEntry::LongText(text, is_centered, is_white) => { + let mut lines = Vec::new(); + let mut line = String::new(); + + // we should probably abstract this away in some capacity + let separator = match state.loc.code.as_str() { + "jp" => "", + _ => " ", + }; + + for word in text.split(separator) { + let combined_word = line.clone() + separator + word; + let line_length = state.font.builder().compute_width(&combined_word) + 32.0; + + if line_length > state.canvas_size.0 as f32 { + lines.push(line); + line = String::new(); + } + + line.push_str(word); + line.push_str(separator); + } + + lines.push(line); + + let mut local_y = y; + + for line in lines.iter() { + let x = if *is_centered { + (state.canvas_size.0 as f32 - state.font.builder().compute_width(&line)) / 2.0 + } else { + self.x as f32 + 20.0 + }; + + let mut builder = state.font.builder().position(x, local_y); + + if !*is_white { + builder = builder.color((0xa0, 0xa0, 0xff, 0xff)); + } + + builder.draw(&line, ctx, &state.constants, &mut state.texture_set)?; + + local_y += entry.height() as f32; + } + + y += entry.height() as f32 * (lines.len() - 1) as f32; + } MenuEntry::Disabled(name) => { state.font.builder().position(self.x as f32 + 20.0, y).color((0xa0, 0xa0, 0xff, 0xff)).draw( name, @@ -412,14 +511,14 @@ impl Menu { let value_text = if let Some(text) = value.get(*index) { text } else { "???" }; let name_text_len = state.font.builder().compute_width(name); - state.font.builder().position(self.x as f32 + 20.0, y).draw( + state.font.builder().position(options_x + 20.0, y).draw( name, ctx, &state.constants, &mut state.texture_set, )?; - state.font.builder().position(self.x as f32 + 25.0 + name_text_len, y).draw( + state.font.builder().position(options_x + 25.0 + name_text_len, y).draw( value_text, ctx, &state.constants, @@ -780,7 +879,12 @@ impl Menu { break; } - sum += entry.height(); + let entry_height = match self.height_overrides.iter().find(|(entry_id, _)| *entry_id == *id) { + Some((_, height)) => *height, + None => entry.height(), + }; + + sum += entry_height; } entry_y = sum as u16; diff --git a/src/menu/pause_menu.rs b/src/menu/pause_menu.rs index 0084501..cf1a1ae 100644 --- a/src/menu/pause_menu.rs +++ b/src/menu/pause_menu.rs @@ -122,12 +122,12 @@ impl PauseMenu { fn update_sizes(&mut self, state: &SharedGameState) { self.pause_menu.update_width(state); - self.pause_menu.update_height(); + self.pause_menu.update_height(state); self.pause_menu.x = ((state.canvas_size.0 - self.pause_menu.width as f32) / 2.0).floor() as isize; self.pause_menu.y = ((state.canvas_size.1 - self.pause_menu.height as f32) / 2.0).floor() as isize; self.confirm_menu.update_width(state); - self.confirm_menu.update_height(); + self.confirm_menu.update_height(state); self.confirm_menu.x = ((state.canvas_size.0 - self.confirm_menu.width as f32) / 2.0).floor() as isize; self.confirm_menu.y = ((state.canvas_size.1 - self.confirm_menu.height as f32) / 2.0).floor() as isize; } diff --git a/src/menu/save_select_menu.rs b/src/menu/save_select_menu.rs index d442e1d..6abe41e 100644 --- a/src/menu/save_select_menu.rs +++ b/src/menu/save_select_menu.rs @@ -217,28 +217,28 @@ impl SaveSelectMenu { fn update_sizes(&mut self, state: &SharedGameState) { self.save_menu.update_width(state); - self.save_menu.update_height(); + self.save_menu.update_height(state); self.save_menu.x = ((state.canvas_size.0 - self.save_menu.width as f32) / 2.0).floor() as isize; self.save_menu.y = ((state.canvas_size.1 - self.save_menu.height as f32) / 2.0).floor() as isize; self.difficulty_menu.update_width(state); - self.difficulty_menu.update_height(); + self.difficulty_menu.update_height(state); self.difficulty_menu.x = ((state.canvas_size.0 - self.difficulty_menu.width as f32) / 2.0).floor() as isize; self.difficulty_menu.y = 30 + ((state.canvas_size.1 - self.difficulty_menu.height as f32) / 2.0).floor() as isize; self.delete_confirm.update_width(state); - self.delete_confirm.update_height(); + self.delete_confirm.update_height(state); self.delete_confirm.x = ((state.canvas_size.0 - self.delete_confirm.width as f32) / 2.0).floor() as isize; self.delete_confirm.y = 30 + ((state.canvas_size.1 - self.delete_confirm.height as f32) / 2.0).floor() as isize; self.load_confirm.update_width(state); - self.load_confirm.update_height(); + self.load_confirm.update_height(state); self.load_confirm.x = ((state.canvas_size.0 - self.load_confirm.width as f32) / 2.0).floor() as isize; self.load_confirm.y = 30 + ((state.canvas_size.1 - self.load_confirm.height as f32) / 2.0).floor() as isize; self.save_detailed.update_width(state); - self.save_detailed.update_height(); + self.save_detailed.update_height(state); self.save_detailed.x = ((state.canvas_size.0 - self.save_detailed.width as f32) / 2.0).floor() as isize; self.save_detailed.y = -40 + ((state.canvas_size.1 - self.save_detailed.height as f32) / 2.0).floor() as isize; } diff --git a/src/menu/settings_menu.rs b/src/menu/settings_menu.rs index a0cac51..dfa1eb6 100644 --- a/src/menu/settings_menu.rs +++ b/src/menu/settings_menu.rs @@ -28,6 +28,7 @@ enum CurrentMenu { BehaviorMenu, LinksMenu, AdvancedMenu, + PortableMenu, } #[derive(Debug, Clone, Copy, Eq, PartialEq)] @@ -143,6 +144,8 @@ enum AdvancedMenuEntry { Title, OpenUserData, OpenGameData, + #[cfg(not(any(target_os = "android", target_os = "horizon")))] + MakePortable, Back, } @@ -152,6 +155,22 @@ impl Default for AdvancedMenuEntry { } } +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +enum PortableMenuEntry { + Title, + Explanation, + RestartQuestion, + Yes, + No, + Spacer, +} + +impl Default for PortableMenuEntry { + fn default() -> Self { + PortableMenuEntry::No + } +} + pub struct SettingsMenu { current: CurrentMenu, main: Menu, @@ -162,6 +181,7 @@ pub struct SettingsMenu { behavior: Menu, links: Menu, advanced: Menu, + portable: Menu, controls_menu: ControlsMenu, pub on_title: bool, } @@ -184,6 +204,7 @@ impl SettingsMenu { let behavior = Menu::new(0, 0, 220, 0); let links = Menu::new(0, 0, 220, 0); let advanced = Menu::new(0, 0, 220, 0); + let portable = Menu::new(0, 0, 220, 0); let controls_menu = ControlsMenu::new(); @@ -198,6 +219,7 @@ impl SettingsMenu { links, advanced, controls_menu, + portable, on_title: false, } } @@ -388,8 +410,56 @@ impl SettingsMenu { AdvancedMenuEntry::OpenGameData, MenuEntry::Active(state.loc.t("menus.options_menu.advanced_menu.open_game_data").to_owned()), ); + + #[cfg(not(any(target_os = "android", target_os = "horizon")))] + if let Some(fs_container) = &state.fs_container { + if !fs_container.is_portable && self.on_title { + self.advanced.push_entry( + AdvancedMenuEntry::MakePortable, + MenuEntry::Active(state.loc.t("menus.options_menu.advanced_menu.make_portable").to_owned()), + ); + } + } + self.advanced.push_entry(AdvancedMenuEntry::Back, MenuEntry::Active(state.loc.t("common.back").to_owned())); + self.portable.center_options = true; + + self.portable.push_entry( + PortableMenuEntry::Title, + MenuEntry::Title(state.loc.t("menus.options_menu.advanced_menu.make_portable").to_owned(), true, false), + ); + + self.portable.push_entry(PortableMenuEntry::Spacer, MenuEntry::Spacer(8.0)); + + self.portable.push_entry( + PortableMenuEntry::Explanation, + MenuEntry::LongText(state.loc.t("menus.options_menu.portable_menu.explanation").to_owned(), true, true), + ); + + self.portable.push_entry(PortableMenuEntry::Spacer, MenuEntry::Spacer(8.0)); + + self.portable.push_entry( + PortableMenuEntry::RestartQuestion, + MenuEntry::LongText( + state.loc.t("menus.options_menu.portable_menu.restart_question").to_owned(), + true, + true, + ), + ); + + self.portable.push_entry(PortableMenuEntry::Spacer, MenuEntry::Spacer(8.0)); + + self.portable.push_entry( + PortableMenuEntry::Yes, + MenuEntry::Active(state.loc.t("menus.options_menu.portable_menu.restart").to_owned()), + ); + + self.portable.push_entry( + PortableMenuEntry::No, + MenuEntry::Active(state.loc.t("menus.options_menu.portable_menu.cancel").to_owned()), + ); + self.main.push_entry(MainMenuEntry::Back, MenuEntry::Active(state.loc.t("common.back").to_owned())); self.sound.push_entry( @@ -524,44 +594,49 @@ impl SettingsMenu { fn update_sizes(&mut self, state: &SharedGameState) { self.main.update_width(state); - self.main.update_height(); + self.main.update_height(state); self.main.x = ((state.canvas_size.0 - self.main.width as f32) / 2.0).floor() as isize; self.main.y = 30 + ((state.canvas_size.1 - self.main.height as f32) / 2.0).floor() as isize; self.graphics.update_width(state); - self.graphics.update_height(); + self.graphics.update_height(state); self.graphics.x = ((state.canvas_size.0 - self.graphics.width as f32) / 2.0).floor() as isize; self.graphics.y = 20 + ((state.canvas_size.1 - self.graphics.height as f32) / 2.0).floor() as isize; self.sound.update_width(state); - self.sound.update_height(); + self.sound.update_height(state); self.sound.x = ((state.canvas_size.0 - self.sound.width as f32) / 2.0).floor() as isize; self.sound.y = 30 + ((state.canvas_size.1 - self.sound.height as f32) / 2.0).floor() as isize; self.soundtrack.update_width(state); - self.soundtrack.update_height(); + self.soundtrack.update_height(state); self.soundtrack.x = ((state.canvas_size.0 - self.soundtrack.width as f32) / 2.0).floor() as isize; self.soundtrack.y = ((state.canvas_size.1 - self.soundtrack.height as f32) / 2.0).floor() as isize; self.language.update_width(state); - self.language.update_height(); + self.language.update_height(state); self.language.x = ((state.canvas_size.0 - self.language.width as f32) / 2.0).floor() as isize; self.language.y = ((state.canvas_size.1 - self.language.height as f32) / 2.0).floor() as isize; self.behavior.update_width(state); - self.behavior.update_height(); + self.behavior.update_height(state); self.behavior.x = ((state.canvas_size.0 - self.behavior.width as f32) / 2.0).floor() as isize; self.behavior.y = 30 + ((state.canvas_size.1 - self.behavior.height as f32) / 2.0).floor() as isize; self.links.update_width(state); - self.links.update_height(); + self.links.update_height(state); self.links.x = ((state.canvas_size.0 - self.links.width as f32) / 2.0).floor() as isize; self.links.y = 30 + ((state.canvas_size.1 - self.links.height as f32) / 2.0).floor() as isize; self.advanced.update_width(state); - self.advanced.update_height(); + self.advanced.update_height(state); self.advanced.x = ((state.canvas_size.0 - self.advanced.width as f32) / 2.0).floor() as isize; self.advanced.y = 30 + ((state.canvas_size.1 - self.advanced.height as f32) / 2.0).floor() as isize; + + self.portable.update_width(state); + self.portable.update_height(state); + self.portable.x = ((state.canvas_size.0 - self.portable.width as f32) / 2.0).floor() as isize; + self.portable.y = 30 + ((state.canvas_size.1 - self.portable.height as f32) / 2.0).floor() as isize; } pub fn tick( @@ -957,11 +1032,26 @@ impl SettingsMenu { fs_container.open_game_directory()?; } } + MenuSelectionResult::Selected(AdvancedMenuEntry::MakePortable, _) => { + self.current = CurrentMenu::PortableMenu; + } MenuSelectionResult::Selected(AdvancedMenuEntry::Back, _) | MenuSelectionResult::Canceled => { self.current = CurrentMenu::MainMenu; } _ => {} }, + CurrentMenu::PortableMenu => match self.portable.tick(controller, state) { + MenuSelectionResult::Selected(PortableMenuEntry::Yes, _) => { + if let Some(fs_container) = &mut state.fs_container { + fs_container.make_portable_user_directory(ctx)?; + state.next_scene = Some(Box::new(TitleScene::new())); + } + } + MenuSelectionResult::Selected(PortableMenuEntry::No, _) | MenuSelectionResult::Canceled => { + self.current = CurrentMenu::AdvancedMenu; + } + _ => {} + }, } Ok(()) } @@ -977,6 +1067,7 @@ impl SettingsMenu { CurrentMenu::BehaviorMenu => self.behavior.draw(state, ctx)?, CurrentMenu::LinksMenu => self.links.draw(state, ctx)?, CurrentMenu::AdvancedMenu => self.advanced.draw(state, ctx)?, + CurrentMenu::PortableMenu => self.portable.draw(state, ctx)?, } Ok(()) diff --git a/src/scene/title_scene.rs b/src/scene/title_scene.rs index 8cd3562..bc8358e 100644 --- a/src/scene/title_scene.rs +++ b/src/scene/title_scene.rs @@ -290,12 +290,12 @@ impl Scene for TitleScene { self.controller.update_trigger(); self.main_menu.update_width(state); - self.main_menu.update_height(); + self.main_menu.update_height(state); self.main_menu.x = ((state.canvas_size.0 - self.main_menu.width as f32) / 2.0).floor() as isize; self.main_menu.y = ((state.canvas_size.1 + 70.0 - self.main_menu.height as f32) / 2.0).floor() as isize; self.challenges_menu.update_width(state); - self.challenges_menu.update_height(); + self.challenges_menu.update_height(state); self.challenges_menu.x = ((state.canvas_size.0 - self.challenges_menu.width as f32) / 2.0).floor() as isize; self.challenges_menu.y = ((state.canvas_size.1 + 30.0 - self.challenges_menu.height as f32) / 2.0).floor() as isize; @@ -446,7 +446,7 @@ impl Scene for TitleScene { } self.confirm_menu.update_width(state); - self.confirm_menu.update_height(); + self.confirm_menu.update_height(state); self.confirm_menu.x = ((state.canvas_size.0 - self.confirm_menu.width as f32) / 2.0).floor() as isize; self.confirm_menu.y = ((state.canvas_size.1 + 30.0 - self.confirm_menu.height as f32) / 2.0).floor() as isize;