diff --git a/src/menu/mod.rs b/src/menu/mod.rs index bbab145..2538690 100644 --- a/src/menu/mod.rs +++ b/src/menu/mod.rs @@ -1,16 +1,19 @@ +use std::cell::Cell; + use crate::common::Rect; use crate::framework::context::Context; use crate::framework::error::GameResult; use crate::input::combined_menu_controller::CombinedMenuController; -use crate::player::skin::basic::BasicPlayerSkin; use crate::shared_game_state::SharedGameState; -use std::cell::Cell; + +pub mod settings_menu; pub struct MenuSaveInfo {} pub enum MenuEntry { Hidden, Active(String), + DisabledWhite(String), Disabled(String), Toggle(String, bool), Options(String, usize, Vec), @@ -23,6 +26,7 @@ impl MenuEntry { match self { MenuEntry::Hidden => 0.0, MenuEntry::Active(_) => 14.0, + MenuEntry::DisabledWhite(_) => 14.0, MenuEntry::Disabled(_) => 14.0, MenuEntry::Toggle(_, _) => 14.0, MenuEntry::Options(_, _, _) => 14.0, @@ -30,6 +34,19 @@ impl MenuEntry { MenuEntry::NewSave => 30.0, } } + + pub fn selectable(&self) -> bool { + match self { + MenuEntry::Hidden => false, + MenuEntry::Active(_) => true, + MenuEntry::DisabledWhite(_) => false, + MenuEntry::Disabled(_) => false, + MenuEntry::Toggle(_, _) => true, + MenuEntry::Options(_, _, _) => true, + MenuEntry::SaveData(_) => true, + MenuEntry::NewSave => true, + } + } } pub enum MenuSelectionResult<'a> { @@ -210,7 +227,7 @@ impl Menu { y = self.y as f32 + 6.0; for entry in self.entries.iter() { match entry { - MenuEntry::Active(name) => { + MenuEntry::Active(name) | MenuEntry::DisabledWhite(name) => { state.font.draw_text( name.chars(), self.x as f32 + 20.0, @@ -253,7 +270,28 @@ impl Menu { ctx, )?; } - MenuEntry::Hidden => {} + MenuEntry::Options(name, index, value) => { + let value_text = if let Some(text) = value.get(*index) { text.as_str() } else { "???" }; + let val_text_len = state.font.text_width(value_text.chars(), &state.constants); + + state.font.draw_text( + name.chars(), + self.x as f32 + 20.0, + y, + &state.constants, + &mut state.texture_set, + ctx, + )?; + + state.font.draw_text( + value_text.chars(), + self.x as f32 + self.width as f32 - val_text_len, + y, + &state.constants, + &mut state.texture_set, + ctx, + )?; + } _ => {} } @@ -289,14 +327,8 @@ impl Menu { } if let Some(entry) = self.entries.get(self.selected) { - match entry { - MenuEntry::Active(_) => { - break; - } - MenuEntry::Toggle(_, _) => { - break; - } - _ => {} + if entry.selectable() { + break; } } else { break; @@ -314,7 +346,7 @@ impl Menu { y += entry.height() as f32; match entry { - MenuEntry::Active(_) | MenuEntry::Toggle(_, _) + MenuEntry::Active(_) | MenuEntry::Toggle(_, _) | MenuEntry::Options(_, _, _) if (self.selected == idx && controller.trigger_ok()) || state.touch_controls.consume_click_in(entry_bounds) => { diff --git a/src/menu/settings_menu.rs b/src/menu/settings_menu.rs new file mode 100644 index 0000000..9e289c6 --- /dev/null +++ b/src/menu/settings_menu.rs @@ -0,0 +1,233 @@ +use crate::framework::context::Context; +use crate::framework::error::GameResult; +use crate::input::combined_menu_controller::CombinedMenuController; +use crate::menu::{Menu, MenuSelectionResult}; +use crate::menu::MenuEntry; +use crate::shared_game_state::{SharedGameState, TimingMode}; +use crate::sound::InterpolationMode; + +#[derive(PartialEq, Eq, Copy, Clone)] +#[repr(u8)] +#[allow(unused)] +enum CurrentMenu { + MainMenu, + GraphicsMenu, + SoundMenu, +} + +pub struct SettingsMenu { + current: CurrentMenu, + main: Menu, + graphics: Menu, + sound: Menu, +} + +static DISCORD_LINK: &str = "https://discord.gg/fbRsNNB"; + +impl SettingsMenu { + pub fn new() -> SettingsMenu { + let main = Menu::new(0, 0, 200, 0); + let graphics = Menu::new(0, 0, 180, 0); + let sound = Menu::new(0, 0, 260, 0); + + SettingsMenu { current: CurrentMenu::MainMenu, main, graphics, sound } + } + + pub fn init(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { + self.graphics.push_entry(MenuEntry::Toggle("Lighting effects:".to_string(), state.settings.shader_effects)); + self.graphics + .push_entry(MenuEntry::Toggle("Motion interpolation:".to_string(), state.settings.motion_interpolation)); + self.graphics.push_entry(MenuEntry::Toggle("Subpixel scrolling:".to_string(), state.settings.subpixel_coords)); + + if state.constants.supports_og_textures { + self.graphics + .push_entry(MenuEntry::Toggle("Original textures".to_string(), state.settings.original_textures)); + } else { + self.graphics.push_entry(MenuEntry::Hidden); + } + + if state.constants.is_cs_plus { + self.graphics + .push_entry(MenuEntry::Toggle("Seasonal textures".to_string(), state.settings.seasonal_textures)); + } else { + self.graphics.push_entry(MenuEntry::Hidden); + } + + self.graphics + .push_entry(MenuEntry::Disabled(format!("Renderer: {}", ctx.renderer.as_ref().unwrap().renderer_name()))); + + self.graphics.push_entry(MenuEntry::Active("< Back".to_owned())); + + self.main.push_entry(MenuEntry::Active("Graphics...".to_owned())); + self.main.push_entry(MenuEntry::Active("Sound...".to_owned())); + + self.main.push_entry(MenuEntry::Options( + "Game timing:".to_owned(), + if state.timing_mode == TimingMode::_50Hz { 0 } else { 1 }, + vec!["50tps (freeware)".to_owned(), "60tps (CS+)".to_owned()], + )); + + self.main.push_entry(MenuEntry::Active(DISCORD_LINK.to_owned())); + + self.main.push_entry(MenuEntry::Active("< Back".to_owned())); + + self.sound.push_entry(MenuEntry::DisabledWhite("BGM Interpolation:".to_owned())); + self.sound.push_entry(MenuEntry::Options( + "".to_owned(), + state.settings.organya_interpolation as usize, + vec![ + "Nearest (fastest, lowest quality)".to_owned(), + "Linear (fast, similar to freeware on Vista+)".to_owned(), + "Cosine".to_owned(), + "Cubic".to_owned(), + "Polyphase (slowest, similar to freeware on XP)".to_owned() + ], + )); + self.sound.push_entry(MenuEntry::Disabled(format!("Soundtrack: {}", state.settings.soundtrack))); + self.sound.push_entry(MenuEntry::Active("< Back".to_owned())); + + self.update_sizes(state); + + Ok(()) + } + + fn update_sizes(&mut self, state: &SharedGameState) { + self.main.update_height(); + 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_height(); + self.graphics.x = ((state.canvas_size.0 - self.graphics.width as f32) / 2.0).floor() as isize; + self.graphics.y = 30 + ((state.canvas_size.1 - self.graphics.height as f32) / 2.0).floor() as isize; + + self.sound.update_height(); + 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; + } + + pub fn tick( + &mut self, + exit_action: &mut dyn FnMut(), + controller: &mut CombinedMenuController, + state: &mut SharedGameState, + ctx: &mut Context, + ) -> GameResult { + self.update_sizes(state); + + match self.current { + CurrentMenu::MainMenu => match self.main.tick(controller, state) { + MenuSelectionResult::Selected(0, _) => { + self.current = CurrentMenu::GraphicsMenu; + } + MenuSelectionResult::Selected(1, _) => { + self.current = CurrentMenu::SoundMenu; + } + MenuSelectionResult::Selected(2, toggle) => { + if let MenuEntry::Options(_, value, _) = toggle { + match state.timing_mode { + TimingMode::_50Hz => { + state.timing_mode = TimingMode::_60Hz; + *value = 1; + } + TimingMode::_60Hz => { + state.timing_mode = TimingMode::_50Hz; + *value = 0; + } + _ => {} + } + let _ = state.settings.save(ctx); + } + } + MenuSelectionResult::Selected(3, _) => { + if let Err(e) = webbrowser::open(DISCORD_LINK) { + log::warn!("Error opening web browser: {}", e); + } + } + MenuSelectionResult::Selected(4, _) | MenuSelectionResult::Canceled => exit_action(), + _ => (), + }, + CurrentMenu::GraphicsMenu => match self.graphics.tick(controller, state) { + MenuSelectionResult::Selected(0, toggle) => { + if let MenuEntry::Toggle(_, value) = toggle { + state.settings.shader_effects = !state.settings.shader_effects; + let _ = state.settings.save(ctx); + + *value = state.settings.shader_effects; + } + } + MenuSelectionResult::Selected(1, toggle) => { + if let MenuEntry::Toggle(_, value) = toggle { + state.settings.motion_interpolation = !state.settings.motion_interpolation; + let _ = state.settings.save(ctx); + + *value = state.settings.motion_interpolation; + } + } + MenuSelectionResult::Selected(2, toggle) => { + if let MenuEntry::Toggle(_, value) = toggle { + state.settings.subpixel_coords = !state.settings.subpixel_coords; + let _ = state.settings.save(ctx); + + *value = state.settings.subpixel_coords; + } + } + MenuSelectionResult::Selected(3, toggle) => { + if let MenuEntry::Toggle(_, value) = toggle { + state.settings.original_textures = !state.settings.original_textures; + state.reload_textures(); + let _ = state.settings.save(ctx); + + *value = state.settings.original_textures; + } + } + MenuSelectionResult::Selected(4, toggle) => { + if let MenuEntry::Toggle(_, value) = toggle { + state.settings.seasonal_textures = !state.settings.seasonal_textures; + state.reload_textures(); + let _ = state.settings.save(ctx); + + *value = state.settings.seasonal_textures; + } + } + MenuSelectionResult::Selected(6, _) | MenuSelectionResult::Canceled => { + self.current = CurrentMenu::MainMenu + } + _ => (), + }, + CurrentMenu::SoundMenu => match self.sound.tick(controller, state) { + MenuSelectionResult::Selected(1, toggle) => { + if let MenuEntry::Options(_, value, _) = toggle { + let (new_mode, new_value) = match *value { + 0 => (InterpolationMode::Linear, 1), + 1 => (InterpolationMode::Cosine, 2), + 2 => (InterpolationMode::Cubic, 3), + 3 => (InterpolationMode::Polyphase, 4), + _ => (InterpolationMode::Nearest, 0), + }; + + *value = new_value; + state.settings.organya_interpolation = new_mode; + state.sound_manager.set_org_interpolation(new_mode); + + let _ = state.settings.save(ctx); + } + } + MenuSelectionResult::Selected(3, _) | MenuSelectionResult::Canceled => { + self.current = CurrentMenu::MainMenu + } + _ => (), + } + } + Ok(()) + } + + pub fn draw(&self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { + match self.current { + CurrentMenu::MainMenu => self.main.draw(state, ctx)?, + CurrentMenu::GraphicsMenu => self.graphics.draw(state, ctx)?, + CurrentMenu::SoundMenu => self.sound.draw(state, ctx)?, + } + + Ok(()) + } +} diff --git a/src/scene/title_scene.rs b/src/scene/title_scene.rs index 90c4ea3..686f053 100644 --- a/src/scene/title_scene.rs +++ b/src/scene/title_scene.rs @@ -4,9 +4,10 @@ use crate::framework::error::GameResult; use crate::framework::graphics; use crate::input::combined_menu_controller::CombinedMenuController; use crate::input::touch_controls::TouchControlType; +use crate::menu::settings_menu::SettingsMenu; use crate::menu::{Menu, MenuEntry, MenuSelectionResult}; use crate::scene::Scene; -use crate::shared_game_state::{SharedGameState, TimingMode}; +use crate::shared_game_state::SharedGameState; #[derive(PartialEq, Eq, Copy, Clone)] #[repr(u8)] @@ -25,7 +26,7 @@ pub struct TitleScene { controller: CombinedMenuController, current_menu: CurrentMenu, main_menu: Menu, - option_menu: Menu, + option_menu: SettingsMenu, save_select_menu: Menu, } @@ -36,7 +37,7 @@ impl TitleScene { controller: CombinedMenuController::new(), current_menu: CurrentMenu::MainMenu, main_menu: Menu::new(0, 0, 100, 0), - option_menu: Menu::new(0, 0, 180, 0), + option_menu: SettingsMenu::new(), save_select_menu: Menu::new(0, 0, 200, 0), } } @@ -85,7 +86,6 @@ impl TitleScene { // asset copyright for freeware version static COPYRIGHT_PIXEL: &str = "2004.12 Studio Pixel"; -static DISCORD_LINK: &str = "https://discord.gg/fbRsNNB"; impl Scene for TitleScene { fn init(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { @@ -103,27 +103,7 @@ impl Scene for TitleScene { } self.main_menu.push_entry(MenuEntry::Active("Quit".to_string())); - self.option_menu.push_entry(MenuEntry::Toggle( - "Original timing (50TPS)".to_string(), - state.timing_mode == TimingMode::_50Hz, - )); - self.option_menu.push_entry(MenuEntry::Toggle("Lighting effects".to_string(), state.settings.shader_effects)); - if state.constants.supports_og_textures { - self.option_menu - .push_entry(MenuEntry::Toggle("Original textures".to_string(), state.settings.original_textures)); - } else { - self.option_menu.push_entry(MenuEntry::Disabled("Original textures".to_string())); - } - - if state.constants.is_cs_plus { - self.option_menu - .push_entry(MenuEntry::Toggle("Seasonal textures".to_string(), state.settings.seasonal_textures)); - } else { - self.option_menu.push_entry(MenuEntry::Disabled("Seasonal textures".to_string())); - } - self.option_menu.push_entry(MenuEntry::Active(DISCORD_LINK.to_owned())); - self.option_menu.push_entry(MenuEntry::Disabled(["Renderer: ", &ctx.renderer.as_ref().unwrap().renderer_name()].join(""))); - self.option_menu.push_entry(MenuEntry::Active("Back".to_string())); + self.option_menu.init(state, ctx)?; self.save_select_menu.push_entry(MenuEntry::NewSave); self.save_select_menu.push_entry(MenuEntry::NewSave); @@ -146,10 +126,6 @@ impl Scene for TitleScene { 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.option_menu.update_height(); - self.option_menu.x = ((state.canvas_size.0 - self.option_menu.width as f32) / 2.0).floor() as isize; - self.option_menu.y = ((state.canvas_size.1 + 70.0 - self.option_menu.height as f32) / 2.0).floor() as isize; - match self.current_menu { CurrentMenu::MainMenu => match self.main_menu.tick(&mut self.controller, state) { MenuSelectionResult::Selected(0, _) => { @@ -171,55 +147,17 @@ impl Scene for TitleScene { } _ => {} }, - CurrentMenu::OptionMenu => match self.option_menu.tick(&mut self.controller, state) { - MenuSelectionResult::Selected(0, toggle) => { - if let MenuEntry::Toggle(_, value) = toggle { - match state.timing_mode { - TimingMode::_50Hz => state.timing_mode = TimingMode::_60Hz, - TimingMode::_60Hz => state.timing_mode = TimingMode::_50Hz, - _ => {} - } - let _ = state.settings.save(ctx); - - *value = state.timing_mode == TimingMode::_50Hz; - } - } - MenuSelectionResult::Selected(1, toggle) => { - if let MenuEntry::Toggle(_, value) = toggle { - state.settings.shader_effects = !state.settings.shader_effects; - let _ = state.settings.save(ctx); - - *value = state.settings.shader_effects; - } - } - MenuSelectionResult::Selected(2, toggle) => { - if let MenuEntry::Toggle(_, value) = toggle { - state.settings.original_textures = !state.settings.original_textures; - state.reload_textures(); - let _ = state.settings.save(ctx); - - *value = state.settings.original_textures; - } - } - MenuSelectionResult::Selected(3, toggle) => { - if let MenuEntry::Toggle(_, value) = toggle { - state.settings.seasonal_textures = !state.settings.seasonal_textures; - state.reload_textures(); - let _ = state.settings.save(ctx); - - *value = state.settings.seasonal_textures; - } - } - MenuSelectionResult::Selected(4, _) => { - if let Err(e) = webbrowser::open(DISCORD_LINK) { - log::warn!("Error opening web browser: {}", e); - } - } - MenuSelectionResult::Selected(6, _) | MenuSelectionResult::Canceled => { - self.current_menu = CurrentMenu::MainMenu; - } - _ => {} - }, + CurrentMenu::OptionMenu => { + let cm = &mut self.current_menu; + self.option_menu.tick( + &mut || { + *cm = CurrentMenu::MainMenu; + }, + &mut self.controller, + state, + ctx, + )?; + } CurrentMenu::StartGame => { if self.tick == 10 { state.reset_skip_flags(); @@ -262,12 +200,8 @@ impl Scene for TitleScene { self.draw_text_centered(COPYRIGHT_PIXEL, state.canvas_size.1 - 30.0, state, ctx)?; match self.current_menu { - CurrentMenu::MainMenu => { - self.main_menu.draw(state, ctx)?; - } - CurrentMenu::OptionMenu => { - self.option_menu.draw(state, ctx)?; - } + CurrentMenu::MainMenu => self.main_menu.draw(state, ctx)?, + CurrentMenu::OptionMenu => self.option_menu.draw(state, ctx)?, _ => {} } diff --git a/src/settings.rs b/src/settings.rs index 1ac9094..3edbdb7 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,5 +1,3 @@ -use serde::{Deserialize, Serialize}; - use crate::framework::context::Context; use crate::framework::error::GameResult; use crate::framework::filesystem::{user_create, user_open}; @@ -8,9 +6,12 @@ use crate::input::keyboard_player_controller::KeyboardController; use crate::input::player_controller::PlayerController; use crate::input::touch_player_controller::TouchPlayerController; use crate::player::TargetPlayer; +use crate::sound::InterpolationMode; -#[derive(Serialize, Deserialize)] +#[derive(serde::Serialize, serde::Deserialize)] pub struct Settings { + #[serde(default = "current_version")] + pub version: u32, pub seasonal_textures: bool, pub original_textures: bool, pub shader_effects: bool, @@ -18,6 +19,8 @@ pub struct Settings { pub motion_interpolation: bool, pub touch_controls: bool, pub soundtrack: String, + #[serde(default = "default_interpolation")] + pub organya_interpolation: InterpolationMode, #[serde(default = "p1_default_keymap")] pub player1_key_map: PlayerKeyMap, #[serde(default = "p2_default_keymap")] @@ -32,6 +35,13 @@ pub struct Settings { pub debug_outlines: bool, } +#[inline(always)] +fn current_version() -> u32 { 2 } + +#[inline(always)] +fn default_interpolation() -> InterpolationMode { InterpolationMode::Linear } + +#[inline(always)] fn default_speed() -> f64 { 1.0 } @@ -70,6 +80,7 @@ impl Settings { impl Default for Settings { fn default() -> Self { Settings { + version: 2, seasonal_textures: true, original_textures: false, shader_effects: true, @@ -77,6 +88,7 @@ impl Default for Settings { motion_interpolation: true, touch_controls: cfg!(target_os = "android"), soundtrack: "".to_string(), + organya_interpolation: InterpolationMode::Linear, player1_key_map: p1_default_keymap(), player2_key_map: p2_default_keymap(), speed: 1.0, @@ -87,7 +99,7 @@ impl Default for Settings { } } -#[derive(Serialize, Deserialize)] +#[derive(serde::Serialize, serde::Deserialize)] pub struct PlayerKeyMap { pub left: ScanCode, pub up: ScanCode, @@ -102,6 +114,7 @@ pub struct PlayerKeyMap { pub map: ScanCode, } +#[inline(always)] fn p1_default_keymap() -> PlayerKeyMap { PlayerKeyMap { left: ScanCode::Left, @@ -118,6 +131,7 @@ fn p1_default_keymap() -> PlayerKeyMap { } } +#[inline(always)] fn p2_default_keymap() -> PlayerKeyMap { PlayerKeyMap { left: ScanCode::Comma, diff --git a/src/sound/mod.rs b/src/sound/mod.rs index 8690e18..6e5ec68 100644 --- a/src/sound/mod.rs +++ b/src/sound/mod.rs @@ -96,6 +96,10 @@ impl SoundManager { let _ = self.tx.send(PlaybackMessage::StopSample(id)); } + pub fn set_org_interpolation(&self, interpolation: InterpolationMode) { + let _ = self.tx.send(PlaybackMessage::SetOrgInterpolation(interpolation)); + } + pub fn play_song( &mut self, song_id: usize, @@ -113,6 +117,7 @@ impl SoundManager { self.prev_song_id = self.current_song_id; self.current_song_id = 0; + self.tx.send(PlaybackMessage::SetOrgInterpolation(settings.organya_interpolation))?; self.tx.send(PlaybackMessage::SaveState)?; self.tx.send(PlaybackMessage::Stop)?; } else if let Some(song_name) = constants.music_table.get(song_id) { @@ -152,6 +157,7 @@ impl SoundManager { self.prev_song_id = self.current_song_id; self.current_song_id = song_id; + self.tx.send(PlaybackMessage::SetOrgInterpolation(settings.organya_interpolation))?; self.tx.send(PlaybackMessage::SaveState)?; self.tx.send(PlaybackMessage::PlayOrganyaSong(Box::new(org)))?; @@ -339,6 +345,7 @@ enum PlaybackMessage { SaveState, RestoreState, SetSampleParams(u8, PixToneParameters), + SetOrgInterpolation(InterpolationMode), } #[derive(PartialEq, Eq)] @@ -520,6 +527,9 @@ where Ok(PlaybackMessage::SetSampleParams(id, params)) => { pixtone.set_sample_parameters(id, params); } + Ok(PlaybackMessage::SetOrgInterpolation(interpolation)) => { + org_engine.interpolation = interpolation; + } Err(_) => { break; } @@ -626,11 +636,11 @@ where if state { if let Err(e) = stream.pause() { - log::error!("Failed to pause the stream: {}", e); + log::error!("Failed to pause the stream: {:?}", e); } } else { if let Err(e) = stream.play() { - log::error!("Failed to unpause the stream: {}", e); + log::error!("Failed to unpause the stream: {:?}", e); } } }