1
0
Fork 0
mirror of https://github.com/doukutsu-rs/doukutsu-rs synced 2024-11-15 18:32:48 +00:00

add portable user directory setting

This commit is contained in:
József Sallai 2023-02-23 14:27:53 +02:00
parent b22ca8b35e
commit 890c0596ed
12 changed files with 319 additions and 53 deletions

View file

@ -133,7 +133,14 @@
"advanced": "Advanced...", "advanced": "Advanced...",
"advanced_menu": { "advanced_menu": {
"open_user_data": "Open user data directory", "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": { "controls_menu": {

View file

@ -133,7 +133,14 @@
"advanced": "詳細設定", "advanced": "詳細設定",
"advanced_menu": { "advanced_menu": {
"open_user_data": "ユーザープロファイルを開く", "open_user_data": "ユーザープロファイルを開く",
"open_game_data": "ゲームファイルを開く" "open_game_data": "ゲームファイルを開く",
"make_portable": "ポータブルユーザーディレクトリを作成する"
},
"portable_menu": {
"explanation": "ローカルのユーザーデータディレクトリが作成され、設定とセーブファイルがそこにコピーされます。",
"restart_question": "新しい場所を使うには、ゲームを再起動しますか?",
"restart": "保存してタイトルに戻る",
"cancel": "キャンセル"
} }
}, },
"controls_menu": { "controls_menu": {

View file

@ -160,7 +160,7 @@ impl Filesystem {
pub(crate) fn user_read_dir<P: AsRef<path::Path>>( pub(crate) fn user_read_dir<P: AsRef<path::Path>>(
&self, &self,
path: P, path: P,
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> { ) -> GameResult<Box<dyn Iterator<Item = path::PathBuf>>> {
let itr = self let itr = self
.user_vfs .user_vfs
.read_dir(path.as_ref())? .read_dir(path.as_ref())?
@ -175,7 +175,7 @@ impl Filesystem {
pub(crate) fn read_dir<P: AsRef<path::Path>>( pub(crate) fn read_dir<P: AsRef<path::Path>>(
&self, &self,
path: P, path: P,
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> { ) -> GameResult<Box<dyn Iterator<Item = path::PathBuf>>> {
let itr = self let itr = self
.vfs .vfs
.read_dir(path.as_ref())? .read_dir(path.as_ref())?
@ -221,6 +221,14 @@ impl Filesystem {
pub fn mount_user_vfs(&mut self, vfs: Box<dyn vfs::VFS>) { pub fn mount_user_vfs(&mut self, vfs: Box<dyn vfs::VFS>) {
self.user_vfs.push_back(vfs); 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` /// Opens the given path and returns the resulting `File`
@ -303,7 +311,7 @@ pub fn user_is_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
pub fn user_read_dir<P: AsRef<path::Path>>( pub fn user_read_dir<P: AsRef<path::Path>>(
ctx: &Context, ctx: &Context,
path: P, path: P,
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> { ) -> GameResult<Box<dyn Iterator<Item = path::PathBuf>>> {
ctx.filesystem.user_read_dir(path) ctx.filesystem.user_read_dir(path)
} }
@ -339,7 +347,7 @@ pub fn is_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
/// in no particular order. /// in no particular order.
/// ///
/// Lists the base directory if an empty path is given. /// Lists the base directory if an empty path is given.
pub fn read_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> { pub fn read_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> GameResult<Box<dyn Iterator<Item = path::PathBuf>>> {
ctx.filesystem.read_dir(path) ctx.filesystem.read_dir(path)
} }
@ -347,7 +355,7 @@ pub fn read_dir_find<P: AsRef<path::Path>>(
ctx: &Context, ctx: &Context,
roots: &Vec<String>, roots: &Vec<String>,
path: P, path: P,
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> { ) -> GameResult<Box<dyn Iterator<Item = path::PathBuf>>> {
let mut files = Vec::new(); let mut files = Vec::new();
for root in roots { for root in roots {
@ -383,3 +391,13 @@ pub fn mount_vfs(ctx: &mut Context, vfs: Box<dyn vfs::VFS>) {
pub fn mount_user_vfs(ctx: &mut Context, vfs: Box<dyn vfs::VFS>) { pub fn mount_user_vfs(ctx: &mut Context, vfs: Box<dyn vfs::VFS>) {
ctx.filesystem.mount_user_vfs(vfs) 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)
}

View file

@ -441,6 +441,11 @@ impl OverlayFS {
pub fn roots(&self) -> &VecDeque<Box<dyn VFS>> { pub fn roots(&self) -> &VecDeque<Box<dyn VFS>> {
&self.roots &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 { impl VFS for OverlayFS {

View file

@ -5,7 +5,7 @@ use crate::{
framework::{ framework::{
context::Context, context::Context,
error::GameResult, error::GameResult,
filesystem::{mount_user_vfs, mount_vfs}, filesystem::{mount_user_vfs, mount_vfs, unmount_user_vfs},
vfs::PhysicalFS, vfs::PhysicalFS,
}, },
}; };
@ -13,11 +13,13 @@ use crate::{
pub struct FilesystemContainer { pub struct FilesystemContainer {
pub user_path: PathBuf, pub user_path: PathBuf,
pub game_path: PathBuf, pub game_path: PathBuf,
pub is_portable: bool,
} }
impl FilesystemContainer { impl FilesystemContainer {
pub fn new() -> Self { 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 { 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")))] #[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(); let mut user_dir = resource_dir.clone();
user_dir.push("_drs_profile"); 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))); mount_user_vfs(context, Box::new(PhysicalFS::new(&user_dir, false)));
self.user_path = user_dir.clone(); self.user_path = user_dir.clone();
self.is_portable = true;
} else { } else {
let user_dir = project_dirs.data_local_dir(); let user_dir = project_dirs.data_local_dir();
mount_user_vfs(context, Box::new(PhysicalFS::new(user_dir, false))); mount_user_vfs(context, Box::new(PhysicalFS::new(user_dir, false)));
@ -155,6 +158,39 @@ impl FilesystemContainer {
self.open_directory(self.game_path.clone()) 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 { fn open_directory(&self, path: PathBuf) -> GameResult {
#[cfg(target_os = "horizon")] #[cfg(target_os = "horizon")]
return Ok(()); // can't open directories on switch return Ok(()); // can't open directories on switch

View file

@ -211,10 +211,8 @@ impl ControlsMenu {
MainMenuEntry::Controller, MainMenuEntry::Controller,
MenuEntry::Active(state.loc.t("menus.controls_menu.controller.entry").to_owned()), MenuEntry::Active(state.loc.t("menus.controls_menu.controller.entry").to_owned()),
); );
self.main.push_entry( self.main
MainMenuEntry::Rebind, .push_entry(MainMenuEntry::Rebind, MenuEntry::Active(state.loc.t("menus.controls_menu.rebind").to_owned()));
MenuEntry::Active(state.loc.t("menus.controls_menu.rebind").to_owned()),
);
self.main.push_entry(MainMenuEntry::Rumble, MenuEntry::Hidden); self.main.push_entry(MainMenuEntry::Rumble, MenuEntry::Hidden);
self.main.push_entry(MainMenuEntry::Back, MenuEntry::Active(state.loc.t("common.back").to_owned())); 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) { fn update_sizes(&mut self, state: &SharedGameState) {
self.main.update_width(state); 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.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.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_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.x = ((state.canvas_size.0 - self.select_controller.width as f32) / 2.0).floor() as isize;
self.select_controller.y = self.select_controller.y =
((state.canvas_size.1 - self.select_controller.height as f32) / 2.0).floor() as isize; ((state.canvas_size.1 - self.select_controller.height as f32) / 2.0).floor() as isize;
self.rebind.update_width(state); 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.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.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_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.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_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_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.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; self.confirm_reset.y = ((state.canvas_size.1 - self.confirm_reset.height as f32) / 2.0).floor() as isize;
} }

View file

@ -96,12 +96,12 @@ impl PlayerCountMenu {
fn update_sizes(&mut self, state: &SharedGameState) { fn update_sizes(&mut self, state: &SharedGameState) {
self.coop_menu.update_width(state); 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.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.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_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.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; self.skin_menu.y = 30 + ((state.canvas_size.1 - self.coop_menu.height as f32) / 2.0).floor() as isize;
} }

View file

@ -28,6 +28,8 @@ pub enum ControlMenuData {
#[derive(Clone)] #[derive(Clone)]
pub enum MenuEntry { pub enum MenuEntry {
Hidden, Hidden,
Title(String, bool, bool), // text, centered, white
LongText(String, bool, bool), // text, centered, white
Active(String), Active(String),
DisabledWhite(String), DisabledWhite(String),
Disabled(String), Disabled(String),
@ -40,12 +42,15 @@ pub enum MenuEntry {
NewSave, NewSave,
PlayerSkin, PlayerSkin,
Control(String, ControlMenuData), Control(String, ControlMenuData),
Spacer(f64),
} }
impl MenuEntry { impl MenuEntry {
pub fn height(&self) -> f64 { pub fn height(&self) -> f64 {
match self { match self {
MenuEntry::Hidden => 0.0, MenuEntry::Hidden => 0.0,
MenuEntry::Title(_, _, _) => 16.0, // individual line
MenuEntry::LongText(_, _, _) => 16.0, // individual line
MenuEntry::Active(_) => 16.0, MenuEntry::Active(_) => 16.0,
MenuEntry::DisabledWhite(_) => 16.0, MenuEntry::DisabledWhite(_) => 16.0,
MenuEntry::Disabled(_) => 16.0, MenuEntry::Disabled(_) => 16.0,
@ -58,12 +63,15 @@ impl MenuEntry {
MenuEntry::NewSave => 32.0, MenuEntry::NewSave => 32.0,
MenuEntry::PlayerSkin => 24.0, MenuEntry::PlayerSkin => 24.0,
MenuEntry::Control(_, _) => 16.0, MenuEntry::Control(_, _) => 16.0,
MenuEntry::Spacer(height) => *height,
} }
} }
pub fn selectable(&self) -> bool { pub fn selectable(&self) -> bool {
match self { match self {
MenuEntry::Hidden => false, MenuEntry::Hidden => false,
MenuEntry::Title(_, _, _) => false,
MenuEntry::LongText(_, _, _) => false,
MenuEntry::Active(_) => true, MenuEntry::Active(_) => true,
MenuEntry::DisabledWhite(_) => false, MenuEntry::DisabledWhite(_) => false,
MenuEntry::Disabled(_) => false, MenuEntry::Disabled(_) => false,
@ -76,6 +84,7 @@ impl MenuEntry {
MenuEntry::NewSave => true, MenuEntry::NewSave => true,
MenuEntry::PlayerSkin => true, MenuEntry::PlayerSkin => true,
MenuEntry::Control(_, _) => true, MenuEntry::Control(_, _) => true,
MenuEntry::Spacer(_) => false,
} }
} }
} }
@ -95,11 +104,13 @@ pub struct Menu<T: std::cmp::PartialEq> {
pub height: u16, pub height: u16,
pub selected: T, pub selected: T,
pub entries: Vec<(T, MenuEntry)>, pub entries: Vec<(T, MenuEntry)>,
pub height_overrides: Vec<(T, f64)>,
anim_num: u16, anim_num: u16,
anim_wait: u16, anim_wait: u16,
custom_cursor: Cell<bool>, custom_cursor: Cell<bool>,
pub draw_cursor: bool, pub draw_cursor: bool,
pub non_interactive: bool, pub non_interactive: bool,
pub center_options: bool,
} }
impl<T: std::cmp::PartialEq + std::default::Default + Clone> Menu<T> { impl<T: std::cmp::PartialEq + std::default::Default + Clone> Menu<T> {
@ -113,9 +124,11 @@ impl<T: std::cmp::PartialEq + std::default::Default + Clone> Menu<T> {
anim_num: 0, anim_num: 0,
anim_wait: 0, anim_wait: 0,
entries: Vec::new(), entries: Vec::new(),
height_overrides: Vec::new(),
custom_cursor: Cell::new(true), custom_cursor: Cell::new(true),
draw_cursor: true, draw_cursor: true,
non_interactive: false, non_interactive: false,
center_options: false,
} }
} }
@ -151,6 +164,10 @@ impl<T: std::cmp::PartialEq + std::default::Default + Clone> Menu<T> {
let entry_width = state.font.builder().compute_width(&entry) + 32.0; let entry_width = state.font.builder().compute_width(&entry) + 32.0;
width = width.max(entry_width); 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, _) => { MenuEntry::Toggle(entry, _) => {
let mut entry_with_option = entry.clone(); let mut entry_with_option = entry.clone();
entry_with_option.push_str(" "); entry_with_option.push_str(" ");
@ -199,19 +216,34 @@ impl<T: std::cmp::PartialEq + std::default::Default + Clone> Menu<T> {
MenuEntry::NewSave => {} MenuEntry::NewSave => {}
MenuEntry::PlayerSkin => {} MenuEntry::PlayerSkin => {}
MenuEntry::Control(_, _) => {} 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 }; 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; let mut height = 8.0;
for (_, entry) in &self.entries { 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(); height += entry.height();
} }
}
}
self.height = height.max(16.0) as u16; self.height = height.max(16.0) as u16;
} }
@ -320,6 +352,26 @@ impl<T: std::cmp::PartialEq + std::default::Default + Clone> Menu<T> {
batch.draw(ctx)?; 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.draw_cursor {
if self.custom_cursor.get() { if self.custom_cursor.get() {
if let Ok(batch) = state.texture_set.get_or_load_batch(ctx, &state.constants, "MenuCursor") { if let Ok(batch) = state.texture_set.get_or_load_batch(ctx, &state.constants, "MenuCursor") {
@ -328,7 +380,7 @@ impl<T: std::cmp::PartialEq + std::default::Default + Clone> Menu<T> {
rect.right = rect.left + 16; rect.right = rect.left + 16;
rect.bottom = rect.top + 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)?; batch.draw(ctx)?;
} else { } else {
@ -365,7 +417,7 @@ impl<T: std::cmp::PartialEq + std::default::Default + Clone> Menu<T> {
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, menu_texture)?; 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)?; batch.draw(ctx)?;
} }
@ -375,13 +427,60 @@ impl<T: std::cmp::PartialEq + std::default::Default + Clone> Menu<T> {
for (_, entry) in &self.entries { for (_, entry) in &self.entries {
match entry { match entry {
MenuEntry::Active(name) | MenuEntry::DisabledWhite(name) => { 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, name,
ctx, ctx,
&state.constants, &state.constants,
&mut state.texture_set, &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) => { MenuEntry::Disabled(name) => {
state.font.builder().position(self.x as f32 + 20.0, y).color((0xa0, 0xa0, 0xff, 0xff)).draw( state.font.builder().position(self.x as f32 + 20.0, y).color((0xa0, 0xa0, 0xff, 0xff)).draw(
name, name,
@ -412,14 +511,14 @@ impl<T: std::cmp::PartialEq + std::default::Default + Clone> Menu<T> {
let value_text = if let Some(text) = value.get(*index) { text } else { "???" }; let value_text = if let Some(text) = value.get(*index) { text } else { "???" };
let name_text_len = state.font.builder().compute_width(name); 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, name,
ctx, ctx,
&state.constants, &state.constants,
&mut state.texture_set, &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, value_text,
ctx, ctx,
&state.constants, &state.constants,
@ -780,7 +879,12 @@ impl<T: std::cmp::PartialEq + std::default::Default + Clone> Menu<T> {
break; 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; entry_y = sum as u16;

View file

@ -122,12 +122,12 @@ impl PauseMenu {
fn update_sizes(&mut self, state: &SharedGameState) { fn update_sizes(&mut self, state: &SharedGameState) {
self.pause_menu.update_width(state); 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.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.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_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.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; self.confirm_menu.y = ((state.canvas_size.1 - self.confirm_menu.height as f32) / 2.0).floor() as isize;
} }

View file

@ -217,28 +217,28 @@ impl SaveSelectMenu {
fn update_sizes(&mut self, state: &SharedGameState) { fn update_sizes(&mut self, state: &SharedGameState) {
self.save_menu.update_width(state); 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.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.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_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.x = ((state.canvas_size.0 - self.difficulty_menu.width as f32) / 2.0).floor() as isize;
self.difficulty_menu.y = self.difficulty_menu.y =
30 + ((state.canvas_size.1 - self.difficulty_menu.height as f32) / 2.0).floor() as isize; 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_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.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.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_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.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.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_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.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; self.save_detailed.y = -40 + ((state.canvas_size.1 - self.save_detailed.height as f32) / 2.0).floor() as isize;
} }

View file

@ -28,6 +28,7 @@ enum CurrentMenu {
BehaviorMenu, BehaviorMenu,
LinksMenu, LinksMenu,
AdvancedMenu, AdvancedMenu,
PortableMenu,
} }
#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
@ -143,6 +144,8 @@ enum AdvancedMenuEntry {
Title, Title,
OpenUserData, OpenUserData,
OpenGameData, OpenGameData,
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
MakePortable,
Back, 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 { pub struct SettingsMenu {
current: CurrentMenu, current: CurrentMenu,
main: Menu<MainMenuEntry>, main: Menu<MainMenuEntry>,
@ -162,6 +181,7 @@ pub struct SettingsMenu {
behavior: Menu<BehaviorMenuEntry>, behavior: Menu<BehaviorMenuEntry>,
links: Menu<LinksMenuEntry>, links: Menu<LinksMenuEntry>,
advanced: Menu<AdvancedMenuEntry>, advanced: Menu<AdvancedMenuEntry>,
portable: Menu<PortableMenuEntry>,
controls_menu: ControlsMenu, controls_menu: ControlsMenu,
pub on_title: bool, pub on_title: bool,
} }
@ -184,6 +204,7 @@ impl SettingsMenu {
let behavior = Menu::new(0, 0, 220, 0); let behavior = Menu::new(0, 0, 220, 0);
let links = Menu::new(0, 0, 220, 0); let links = Menu::new(0, 0, 220, 0);
let advanced = 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(); let controls_menu = ControlsMenu::new();
@ -198,6 +219,7 @@ impl SettingsMenu {
links, links,
advanced, advanced,
controls_menu, controls_menu,
portable,
on_title: false, on_title: false,
} }
} }
@ -388,8 +410,56 @@ impl SettingsMenu {
AdvancedMenuEntry::OpenGameData, AdvancedMenuEntry::OpenGameData,
MenuEntry::Active(state.loc.t("menus.options_menu.advanced_menu.open_game_data").to_owned()), 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.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.main.push_entry(MainMenuEntry::Back, MenuEntry::Active(state.loc.t("common.back").to_owned()));
self.sound.push_entry( self.sound.push_entry(
@ -524,44 +594,49 @@ impl SettingsMenu {
fn update_sizes(&mut self, state: &SharedGameState) { fn update_sizes(&mut self, state: &SharedGameState) {
self.main.update_width(state); 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.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.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_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.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.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_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.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.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_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.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.soundtrack.y = ((state.canvas_size.1 - self.soundtrack.height as f32) / 2.0).floor() as isize;
self.language.update_width(state); 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.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.language.y = ((state.canvas_size.1 - self.language.height as f32) / 2.0).floor() as isize;
self.behavior.update_width(state); 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.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.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_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.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.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_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.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.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( pub fn tick(
@ -957,11 +1032,26 @@ impl SettingsMenu {
fs_container.open_game_directory()?; fs_container.open_game_directory()?;
} }
} }
MenuSelectionResult::Selected(AdvancedMenuEntry::MakePortable, _) => {
self.current = CurrentMenu::PortableMenu;
}
MenuSelectionResult::Selected(AdvancedMenuEntry::Back, _) | MenuSelectionResult::Canceled => { MenuSelectionResult::Selected(AdvancedMenuEntry::Back, _) | MenuSelectionResult::Canceled => {
self.current = CurrentMenu::MainMenu; 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(()) Ok(())
} }
@ -977,6 +1067,7 @@ impl SettingsMenu {
CurrentMenu::BehaviorMenu => self.behavior.draw(state, ctx)?, CurrentMenu::BehaviorMenu => self.behavior.draw(state, ctx)?,
CurrentMenu::LinksMenu => self.links.draw(state, ctx)?, CurrentMenu::LinksMenu => self.links.draw(state, ctx)?,
CurrentMenu::AdvancedMenu => self.advanced.draw(state, ctx)?, CurrentMenu::AdvancedMenu => self.advanced.draw(state, ctx)?,
CurrentMenu::PortableMenu => self.portable.draw(state, ctx)?,
} }
Ok(()) Ok(())

View file

@ -290,12 +290,12 @@ impl Scene for TitleScene {
self.controller.update_trigger(); self.controller.update_trigger();
self.main_menu.update_width(state); 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.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.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_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.x = ((state.canvas_size.0 - self.challenges_menu.width as f32) / 2.0).floor() as isize;
self.challenges_menu.y = self.challenges_menu.y =
((state.canvas_size.1 + 30.0 - self.challenges_menu.height as f32) / 2.0).floor() as isize; ((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_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.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; self.confirm_menu.y = ((state.canvas_size.1 + 30.0 - self.confirm_menu.height as f32) / 2.0).floor() as isize;