diff --git a/src/builtin/locale/en.json b/src/builtin/locale/en.json index ce901b3..436e674 100644 --- a/src/builtin/locale/en.json +++ b/src/builtin/locale/en.json @@ -116,10 +116,14 @@ "language": "Language...", - "game_timing": { - "entry": "Game timing:", - "50tps": "50tps (freeware)", - "60tps": "60tps (CS+)" + "behavior": "Behavior...", + "behavior_menu": { + "game_timing": { + "entry": "Game timing:", + "50tps": "50tps (freeware)", + "60tps": "60tps (CS+)" + }, + "pause_on_focus_loss": "Pause on focus loss:" } } }, diff --git a/src/builtin/locale/jp.json b/src/builtin/locale/jp.json index 0f09bfd..c09947e 100644 --- a/src/builtin/locale/jp.json +++ b/src/builtin/locale/jp.json @@ -105,11 +105,17 @@ }, "soundtrack": "サウンドトラック: {soundtrack}" }, + "language": "言語", - "game_timing": { - "entry": "ゲームのタイミング:", - "50tps": "50tps (freeware)", - "60tps": "60tps (CS+)" + + "behavior": "動作", + "behavior_menu": { + "game_timing": { + "entry": "ゲームのタイミング:", + "50tps": "50tps (freeware)", + "60tps": "60tps (CS+)" + }, + "pause_on_focus_loss": "フォーカスが外れた時のポーズ:" } } }, diff --git a/src/framework/backend_sdl2.rs b/src/framework/backend_sdl2.rs index 6147324..818afe6 100644 --- a/src/framework/backend_sdl2.rs +++ b/src/framework/backend_sdl2.rs @@ -237,8 +237,25 @@ impl BackendEventLoop for SDL2EventLoop { state.shutdown(); } Event::Window { win_event, .. } => match win_event { - WindowEvent::FocusGained | WindowEvent::Shown => {} - WindowEvent::FocusLost | WindowEvent::Hidden => {} + WindowEvent::FocusGained | WindowEvent::Shown => { + if state.settings.pause_on_focus_loss { + { + let mut mutex = GAME_SUSPENDED.lock().unwrap(); + *mutex = false; + } + + state.sound_manager.resume(); + game.loops = 0; + } + } + WindowEvent::FocusLost | WindowEvent::Hidden => { + if state.settings.pause_on_focus_loss { + let mut mutex = GAME_SUSPENDED.lock().unwrap(); + *mutex = true; + + state.sound_manager.pause(); + } + } WindowEvent::SizeChanged(width, height) => { ctx.screen_size = (width.max(1) as f32, height.max(1) as f32); diff --git a/src/menu/settings_menu.rs b/src/menu/settings_menu.rs index c9a550b..ac1a031 100644 --- a/src/menu/settings_menu.rs +++ b/src/menu/settings_menu.rs @@ -22,6 +22,7 @@ enum CurrentMenu { SoundMenu, SoundtrackMenu, LanguageMenu, + BehaviorMenu, } #[derive(Debug, Clone, Copy, Eq, PartialEq)] @@ -29,7 +30,7 @@ enum MainMenuEntry { Graphics, Sound, Language, - GameTiming, + Behavior, DiscordLink, Back, } @@ -101,6 +102,19 @@ impl Default for LanguageMenuEntry { } } +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +enum BehaviorMenuEntry { + GameTiming, + PauseOnFocusLoss, + Back, +} + +impl Default for BehaviorMenuEntry { + fn default() -> Self { + BehaviorMenuEntry::GameTiming + } +} + pub struct SettingsMenu { current: CurrentMenu, main: Menu, @@ -108,6 +122,7 @@ pub struct SettingsMenu { sound: Menu, soundtrack: Menu, language: Menu, + behavior: Menu, pub on_title: bool, } @@ -120,8 +135,18 @@ impl SettingsMenu { let sound = Menu::new(0, 0, 260, 0); let soundtrack = Menu::new(0, 0, 260, 0); let language = Menu::new(0, 0, 120, 0); + let behavior = Menu::new(0, 0, 220, 0); - SettingsMenu { current: CurrentMenu::MainMenu, main, graphics, sound, soundtrack, language, on_title: false } + SettingsMenu { + current: CurrentMenu::MainMenu, + main, + graphics, + sound, + soundtrack, + language, + behavior, + on_title: false, + } } pub fn init(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { @@ -251,14 +276,7 @@ impl SettingsMenu { self.main.push_entry(MainMenuEntry::Language, MenuEntry::Active(state.t("menus.options_menu.language"))); } - self.main.push_entry( - MainMenuEntry::GameTiming, - MenuEntry::Options( - state.t("menus.options_menu.game_timing.entry"), - if state.settings.timing_mode == TimingMode::_50Hz { 0 } else { 1 }, - vec![state.t("menus.options_menu.game_timing.50tps"), state.t("menus.options_menu.game_timing.60tps")], - ), - ); + self.main.push_entry(MainMenuEntry::Behavior, MenuEntry::Active(state.t("menus.options_menu.behavior"))); self.main.push_entry(MainMenuEntry::DiscordLink, MenuEntry::Active(DISCORD_LINK.to_owned())); @@ -334,6 +352,28 @@ impl SettingsMenu { self.soundtrack.push_entry(SoundtrackMenuEntry::Back, MenuEntry::Active(state.t("common.back"))); + self.behavior.push_entry( + BehaviorMenuEntry::GameTiming, + MenuEntry::Options( + state.t("menus.options_menu.behavior_menu.game_timing.entry"), + if state.settings.timing_mode == TimingMode::_50Hz { 0 } else { 1 }, + vec![ + state.t("menus.options_menu.behavior_menu.game_timing.50tps"), + state.t("menus.options_menu.behavior_menu.game_timing.60tps"), + ], + ), + ); + + self.behavior.push_entry( + BehaviorMenuEntry::PauseOnFocusLoss, + MenuEntry::Toggle( + state.t("menus.options_menu.behavior_menu.pause_on_focus_loss"), + state.settings.pause_on_focus_loss, + ), + ); + + self.behavior.push_entry(BehaviorMenuEntry::Back, MenuEntry::Active(state.t("common.back"))); + self.update_sizes(state); Ok(()) @@ -364,6 +404,11 @@ impl SettingsMenu { self.language.update_height(); 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.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; } pub fn tick( @@ -387,21 +432,8 @@ impl SettingsMenu { self.language.selected = LanguageMenuEntry::Language(state.settings.locale); self.current = CurrentMenu::LanguageMenu; } - MenuSelectionResult::Selected(MainMenuEntry::GameTiming, toggle) => { - if let MenuEntry::Options(_, value, _) = toggle { - match state.settings.timing_mode { - TimingMode::_50Hz => { - state.settings.timing_mode = TimingMode::_60Hz; - *value = 1; - } - TimingMode::_60Hz => { - state.settings.timing_mode = TimingMode::_50Hz; - *value = 0; - } - _ => {} - } - let _ = state.settings.save(ctx); - } + MenuSelectionResult::Selected(MainMenuEntry::Behavior, _) => { + self.current = CurrentMenu::BehaviorMenu; } MenuSelectionResult::Selected(MainMenuEntry::DiscordLink, _) => { if let Err(e) = webbrowser::open(DISCORD_LINK) { @@ -655,6 +687,36 @@ impl SettingsMenu { } _ => (), }, + CurrentMenu::BehaviorMenu => match self.behavior.tick(controller, state) { + MenuSelectionResult::Selected(BehaviorMenuEntry::GameTiming, toggle) => { + if let MenuEntry::Options(_, value, _) = toggle { + match state.settings.timing_mode { + TimingMode::_50Hz => { + state.settings.timing_mode = TimingMode::_60Hz; + *value = 1; + } + TimingMode::_60Hz => { + state.settings.timing_mode = TimingMode::_50Hz; + *value = 0; + } + _ => {} + } + let _ = state.settings.save(ctx); + } + } + MenuSelectionResult::Selected(BehaviorMenuEntry::PauseOnFocusLoss, toggle) => { + if let MenuEntry::Toggle(_, value) = toggle { + state.settings.pause_on_focus_loss = !state.settings.pause_on_focus_loss; + let _ = state.settings.save(ctx); + + *value = state.settings.pause_on_focus_loss; + } + } + MenuSelectionResult::Selected(BehaviorMenuEntry::Back, _) | MenuSelectionResult::Canceled => { + self.current = CurrentMenu::MainMenu; + } + _ => (), + }, } Ok(()) } @@ -666,6 +728,7 @@ impl SettingsMenu { CurrentMenu::SoundMenu => self.sound.draw(state, ctx)?, CurrentMenu::SoundtrackMenu => self.soundtrack.draw(state, ctx)?, CurrentMenu::LanguageMenu => self.language.draw(state, ctx)?, + CurrentMenu::BehaviorMenu => self.behavior.draw(state, ctx)?, } Ok(()) diff --git a/src/scene/jukebox_scene.rs b/src/scene/jukebox_scene.rs index c9b4156..757689a 100644 --- a/src/scene/jukebox_scene.rs +++ b/src/scene/jukebox_scene.rs @@ -24,6 +24,7 @@ pub struct JukeboxScene { frame: Frame, stage: Stage, textures: StageTexturePaths, + previous_pause_on_focus_loss_setting: bool, } impl JukeboxScene { @@ -58,6 +59,7 @@ impl JukeboxScene { frame: Frame::new(), stage: fake_stage, textures, + previous_pause_on_focus_loss_setting: true, } } } @@ -97,6 +99,9 @@ impl Scene for JukeboxScene { self.soundtracks.iter().position(|s| s == &state.settings.soundtrack).unwrap_or(0); self.selected_soundtrack = selected_soundtrack_index; + self.previous_pause_on_focus_loss_setting = state.settings.pause_on_focus_loss; + state.settings.pause_on_focus_loss = false; + Ok(()) } @@ -151,6 +156,7 @@ impl Scene for JukeboxScene { } if self.controller.trigger_back() { + state.settings.pause_on_focus_loss = self.previous_pause_on_focus_loss_setting; state.next_scene = Some(Box::new(TitleScene::new())); } diff --git a/src/settings.rs b/src/settings.rs index f986ee7..364b3b5 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -34,6 +34,8 @@ pub struct Settings { pub sfx_volume: f32, #[serde(default = "default_timing")] pub timing_mode: TimingMode, + #[serde(default = "default_pause_on_focus_loss")] + pub pause_on_focus_loss: bool, #[serde(default = "default_interpolation")] pub organya_interpolation: InterpolationMode, #[serde(default = "default_controller_type")] @@ -79,7 +81,7 @@ fn default_true() -> bool { #[inline(always)] fn current_version() -> u32 { - 14 + 15 } #[inline(always)] @@ -127,6 +129,11 @@ fn default_controller_type() -> ControllerType { ControllerType::Keyboard } +#[inline(always)] +fn default_pause_on_focus_loss() -> bool { + true +} + impl Settings { pub fn load(ctx: &Context) -> GameResult { if let Ok(file) = user_open(ctx, "/settings.json") { @@ -223,6 +230,11 @@ impl Settings { self.player2_controller_button_map = player_default_controller_button_map(); } + if self.version == 14 { + self.version = 15; + self.pause_on_focus_loss = default_pause_on_focus_loss(); + } + if self.version != initial_version { log::info!("Upgraded configuration file from version {} to {}.", initial_version, self.version); } @@ -281,6 +293,7 @@ impl Default for Settings { bgm_volume: 1.0, sfx_volume: 1.0, timing_mode: default_timing(), + pause_on_focus_loss: default_pause_on_focus_loss(), organya_interpolation: InterpolationMode::Linear, player1_controller_type: default_controller_type(), player2_controller_type: default_controller_type(),