// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team // SPDX-License-Identifier: GPL-3.0+ #include "Achievements.h" #include "GS.h" #include "Host.h" #include "IconsFontAwesome.h" #include "ImGui/FullscreenUI.h" #include "ImGui/ImGuiOverlays.h" #include "Input/InputManager.h" #include "Recording/InputRecording.h" #include "SPU2/spu2.h" #include "VMManager.h" #include "SIO/Memcard/MemoryCardFile.h" #include "common/Assertions.h" #include "common/Error.h" #include "common/FileSystem.h" #include "common/Path.h" #include "common/Timer.h" static std::optional s_limiter_mode_prior_to_hold_interaction; void VMManager::Internal::ResetVMHotkeyState() { s_limiter_mode_prior_to_hold_interaction.reset(); } static void HotkeyAdjustTargetSpeed(double delta) { const double min_speed = Achievements::IsHardcoreModeActive() ? 1.0 : 0.1; EmuConfig.EmulationSpeed.NominalScalar = std::max(min_speed, EmuConfig.EmulationSpeed.NominalScalar + delta); if (VMManager::GetLimiterMode() != LimiterModeType::Nominal) VMManager::SetLimiterMode(LimiterModeType::Nominal); else VMManager::UpdateTargetSpeed(); Host::AddIconOSDMessage("SpeedChanged", ICON_FA_CLOCK, fmt::format(TRANSLATE_FS("Hotkeys", "Target speed set to {:.0f}%."), std::round(EmuConfig.EmulationSpeed.NominalScalar * 100.0)), Host::OSD_QUICK_DURATION); } static void HotkeyAdjustVolume(const s32 delta) { if (!VMManager::HasValidVM()) return; // Volume-adjusting hotkeys override mute toggle hotkey. EmuConfig.SPU2.OutputMuted overrides hotkeys. if (!SPU2::SetOutputMuted(false)) { Host::AddIconOSDMessage("VolumeChanged", ICON_FA_VOLUME_XMARK, TRANSLATE_STR("Hotkeys", "Volume: Muted in Settings")); return; } const s32 current_volume = static_cast(SPU2::GetOutputVolume()); const s32 maximum_volume = static_cast(Pcsx2Config::SPU2Options::MAX_VOLUME); const s32 new_volume = std::clamp(current_volume + delta, 0, maximum_volume); if (current_volume != new_volume) SPU2::SetOutputVolume(static_cast(new_volume)); if (new_volume > 0 && new_volume < maximum_volume) { Host::AddIconOSDMessage("VolumeChanged", new_volume < 100 ? ICON_FA_VOLUME_LOW : ICON_FA_VOLUME_HIGH, fmt::format(TRANSLATE_FS("Hotkeys", "Volume: {} to {}%"), delta < 0 ? TRANSLATE_STR("Hotkeys", "Decreased") : TRANSLATE_STR("Hotkeys", "Increased"), new_volume)); } else { Host::AddIconOSDMessage("VolumeChanged", delta < 0 ? ICON_FA_VOLUME_OFF : ICON_FA_VOLUME_HIGH, fmt::format(TRANSLATE_FS("Hotkeys", "Volume: {} {}% Reached"), delta < 0 ? TRANSLATE_STR("Hotkeys", "Minimum") : TRANSLATE_STR("Hotkeys", "Maximum"), new_volume)); } } static void HotkeyToggleMute() { if (!VMManager::HasValidVM()) return; // Attempt to toggle output muting. EmuConfig.SPU2.OutputMuted overrides hotkeys. if (SPU2::SetOutputMuted(!SPU2::IsOutputMuted())) { if (SPU2::IsOutputMuted()) Host::AddIconOSDMessage("VolumeChanged", ICON_FA_VOLUME_XMARK, TRANSLATE_STR("Hotkeys", "Volume: Muted")); else { const u32 current_volume = SPU2::GetOutputVolume(); Host::AddIconOSDMessage("VolumeChanged", current_volume < 100 ? (current_volume == 0 ? ICON_FA_VOLUME_OFF : ICON_FA_VOLUME_LOW) : ICON_FA_VOLUME_HIGH, fmt::format(TRANSLATE_FS("Hotkeys", "Volume: Unmuted to {}%"), current_volume)); } } else Host::AddIconOSDMessage("VolumeChanged", ICON_FA_VOLUME_XMARK, TRANSLATE_STR("Hotkeys", "Volume: Muted in Settings")); } static void HotkeyLoadStateSlot(s32 slot) { // Can reapply settings and thus binds, therefore must be deferred. Host::RunOnCPUThread([slot]() { if (!VMManager::HasSaveStateInSlot(VMManager::GetDiscSerial().c_str(), VMManager::GetDiscCRC(), slot)) { Host::AddIconOSDMessage("LoadStateFromSlot", ICON_FA_TRIANGLE_EXCLAMATION, fmt::format(TRANSLATE_FS("Hotkeys", "No save state found in slot {}."), slot), Host::OSD_INFO_DURATION); return; } Error error; if (!VMManager::LoadStateFromSlot(slot, false, &error)) FullscreenUI::ReportStateLoadError(error.GetDescription(), slot, false); }); } static void HotkeySaveStateSlot(s32 slot) { VMManager::SaveStateToSlot(slot, true, [slot](const std::string& error) { FullscreenUI::ReportStateSaveError(error, slot); }); } static bool CanPause() { static constexpr const float PAUSE_INTERVAL = 3.0f; static Common::Timer::Value s_last_pause_time = 0; if (!Achievements::IsHardcoreModeActive() || VMManager::GetState() == VMState::Paused) return true; const Common::Timer::Value time = Common::Timer::GetCurrentValue(); const float delta = static_cast(Common::Timer::ConvertValueToSeconds(time - s_last_pause_time)); if (delta < PAUSE_INTERVAL) { Host::AddIconOSDMessage("PauseCooldown", ICON_FA_CLOCK, TRANSLATE_PLURAL_STR("Hotkeys", "You cannot pause until another %n second(s) have passed.", "", static_cast(std::ceil(PAUSE_INTERVAL - delta))), Host::OSD_QUICK_DURATION); return false; } Host::RemoveKeyedOSDMessage("PauseCooldown"); s_last_pause_time = time; return true; } static bool UseSavestateSelector() { return EmuConfig.UseSavestateSelector; } BEGIN_HOTKEY_LIST(g_common_hotkeys) DEFINE_HOTKEY("ToggleFullscreen", TRANSLATE_NOOP("Hotkeys", "Navigation"), TRANSLATE_NOOP("Hotkeys", "Toggle Fullscreen"), [](s32 pressed) { if (!pressed) Host::SetFullscreen(!Host::IsFullscreen()); }) DEFINE_HOTKEY("OpenPauseMenu", TRANSLATE_NOOP("Hotkeys", "Navigation"), TRANSLATE_NOOP("Hotkeys", "Open Pause Menu"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM() && CanPause()) FullscreenUI::OpenPauseMenu(); }) DEFINE_HOTKEY("OpenAchievementsList", TRANSLATE_NOOP("Hotkeys", "Navigation"), TRANSLATE_NOOP("Hotkeys", "Open Achievements List"), [](s32 pressed) { if (!pressed && CanPause()) FullscreenUI::OpenAchievementsWindow(); }) DEFINE_HOTKEY("OpenLeaderboardsList", TRANSLATE_NOOP("Hotkeys", "Navigation"), TRANSLATE_NOOP("Hotkeys", "Open Leaderboards List"), [](s32 pressed) { if (!pressed && CanPause()) FullscreenUI::OpenLeaderboardsWindow(); }) DEFINE_HOTKEY( "TogglePause", TRANSLATE_NOOP("Hotkeys", "Speed"), TRANSLATE_NOOP("Hotkeys", "Toggle Pause"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM() && CanPause()) VMManager::SetPaused(VMManager::GetState() != VMState::Paused); }) DEFINE_HOTKEY( "FrameAdvance", TRANSLATE_NOOP("Hotkeys", "Speed"), TRANSLATE_NOOP("Hotkeys", "Frame Advance"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) VMManager::FrameAdvance(1); }) DEFINE_HOTKEY("ToggleFrameLimit", TRANSLATE_NOOP("Hotkeys", "Speed"), TRANSLATE_NOOP("Hotkeys", "Toggle Frame Limit"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) { VMManager::SetLimiterMode((VMManager::GetLimiterMode() != LimiterModeType::Unlimited) ? LimiterModeType::Unlimited : LimiterModeType::Nominal); } }) DEFINE_HOTKEY("ToggleTurbo", TRANSLATE_NOOP("Hotkeys", "Speed"), TRANSLATE_NOOP("Hotkeys", "Toggle Turbo / Fast Forward"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) { VMManager::SetLimiterMode( (VMManager::GetLimiterMode() != LimiterModeType::Turbo) ? LimiterModeType::Turbo : LimiterModeType::Nominal); } }) DEFINE_HOTKEY("HoldTurbo", TRANSLATE_NOOP("Hotkeys", "Speed"), TRANSLATE_NOOP("Hotkeys", "Turbo / Fast Forward (Hold)"), [](s32 pressed) { if (!VMManager::HasValidVM()) return; if (pressed > 0 && !s_limiter_mode_prior_to_hold_interaction.has_value()) { s_limiter_mode_prior_to_hold_interaction = VMManager::GetLimiterMode(); VMManager::SetLimiterMode((s_limiter_mode_prior_to_hold_interaction.value() != LimiterModeType::Turbo) ? LimiterModeType::Turbo : LimiterModeType::Nominal); } else if (pressed >= 0 && s_limiter_mode_prior_to_hold_interaction.has_value()) { VMManager::SetLimiterMode(s_limiter_mode_prior_to_hold_interaction.value()); s_limiter_mode_prior_to_hold_interaction.reset(); } }) DEFINE_HOTKEY("ToggleSlowMotion", TRANSLATE_NOOP("Hotkeys", "Speed"), TRANSLATE_NOOP("Hotkeys", "Toggle Slow Motion"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) { VMManager::SetLimiterMode( (VMManager::GetLimiterMode() != LimiterModeType::Slomo) ? LimiterModeType::Slomo : LimiterModeType::Nominal); } }) DEFINE_HOTKEY("IncreaseSpeed", TRANSLATE_NOOP("Hotkeys", "Speed"), TRANSLATE_NOOP("Hotkeys", "Increase Target Speed"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) HotkeyAdjustTargetSpeed(0.1); }) DEFINE_HOTKEY("DecreaseSpeed", TRANSLATE_NOOP("Hotkeys", "Speed"), TRANSLATE_NOOP("Hotkeys", "Decrease Target Speed"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) HotkeyAdjustTargetSpeed(-0.1); }) DEFINE_HOTKEY("ShutdownVM", TRANSLATE_NOOP("Hotkeys", "System"), TRANSLATE_NOOP("Hotkeys", "Shut Down Virtual Machine"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) Host::RequestVMShutdown(true, true, EmuConfig.SaveStateOnShutdown); }) DEFINE_HOTKEY("ResetVM", TRANSLATE_NOOP("Hotkeys", "System"), TRANSLATE_NOOP("Hotkeys", "Reset Virtual Machine"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) VMManager::Reset(); }) DEFINE_HOTKEY("SwapMemCards", TRANSLATE_NOOP("Hotkeys", "System"), TRANSLATE_NOOP("Hotkeys", "Swap Memory Cards"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) Host::RunOnCPUThread([]() { FileMcd_Swap(); }); }) DEFINE_HOTKEY("InputRecToggleMode", TRANSLATE_NOOP("Hotkeys", "System"), TRANSLATE_NOOP("Hotkeys", "Toggle Input Recording Mode"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) g_InputRecording.getControls().toggleRecordMode(); }) DEFINE_HOTKEY("PreviousSaveStateSlot", TRANSLATE_NOOP("Hotkeys", "Save States"), TRANSLATE_NOOP("Hotkeys", "Select Previous Save Slot"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) SaveStateSelectorUI::SelectPreviousSlot(UseSavestateSelector()); }) DEFINE_HOTKEY("NextSaveStateSlot", TRANSLATE_NOOP("Hotkeys", "Save States"), TRANSLATE_NOOP("Hotkeys", "Select Next Save Slot"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) SaveStateSelectorUI::SelectNextSlot(UseSavestateSelector()); }) DEFINE_HOTKEY("SaveStateToSlot", TRANSLATE_NOOP("Hotkeys", "Save States"), TRANSLATE_NOOP("Hotkeys", "Save State To Selected Slot"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) SaveStateSelectorUI::SaveCurrentSlot(); }) DEFINE_HOTKEY("LoadStateFromSlot", TRANSLATE_NOOP("Hotkeys", "Save States"), TRANSLATE_NOOP("Hotkeys", "Load State From Selected Slot"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) SaveStateSelectorUI::LoadCurrentSlot(); }) DEFINE_HOTKEY("LoadBackupStateFromSlot", TRANSLATE_NOOP("Hotkeys", "Save States"), TRANSLATE_NOOP("Hotkeys", "Load Backup State From Selected Slot"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) SaveStateSelectorUI::LoadCurrentBackupSlot(); }) DEFINE_HOTKEY("SaveStateAndSelectNextSlot", TRANSLATE_NOOP("Hotkeys", "Save States"), TRANSLATE_NOOP("Hotkeys", "Save State and Select Next Slot"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) { SaveStateSelectorUI::SaveCurrentSlot(); SaveStateSelectorUI::SelectNextSlot(false); } }) DEFINE_HOTKEY("SelectNextSlotAndSaveState", TRANSLATE_NOOP("Hotkeys", "Save States"), TRANSLATE_NOOP("Hotkeys", "Select Next Slot and Save State"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) { SaveStateSelectorUI::SelectNextSlot(false); SaveStateSelectorUI::SaveCurrentSlot(); } }) #define DEFINE_HOTKEY_SAVESTATE_X(slotnum, title) \ DEFINE_HOTKEY("SaveStateToSlot" #slotnum, "Save States", title, [](s32 pressed) { \ if (!pressed) \ HotkeySaveStateSlot(slotnum); \ }) #define DEFINE_HOTKEY_LOADSTATE_X(slotnum, title) \ DEFINE_HOTKEY("LoadStateFromSlot" #slotnum, "Save States", title, [](s32 pressed) { \ if (!pressed) \ HotkeyLoadStateSlot(slotnum); \ }) DEFINE_HOTKEY_SAVESTATE_X(1, TRANSLATE_NOOP("Hotkeys", "Save State To Slot 1")) DEFINE_HOTKEY_LOADSTATE_X(1, TRANSLATE_NOOP("Hotkeys", "Load State From Slot 1")) DEFINE_HOTKEY_SAVESTATE_X(2, TRANSLATE_NOOP("Hotkeys", "Save State To Slot 2")) DEFINE_HOTKEY_LOADSTATE_X(2, TRANSLATE_NOOP("Hotkeys", "Load State From Slot 2")) DEFINE_HOTKEY_SAVESTATE_X(3, TRANSLATE_NOOP("Hotkeys", "Save State To Slot 3")) DEFINE_HOTKEY_LOADSTATE_X(3, TRANSLATE_NOOP("Hotkeys", "Load State From Slot 3")) DEFINE_HOTKEY_SAVESTATE_X(4, TRANSLATE_NOOP("Hotkeys", "Save State To Slot 4")) DEFINE_HOTKEY_LOADSTATE_X(4, TRANSLATE_NOOP("Hotkeys", "Load State From Slot 4")) DEFINE_HOTKEY_SAVESTATE_X(5, TRANSLATE_NOOP("Hotkeys", "Save State To Slot 5")) DEFINE_HOTKEY_LOADSTATE_X(5, TRANSLATE_NOOP("Hotkeys", "Load State From Slot 5")) DEFINE_HOTKEY_SAVESTATE_X(6, TRANSLATE_NOOP("Hotkeys", "Save State To Slot 6")) DEFINE_HOTKEY_LOADSTATE_X(6, TRANSLATE_NOOP("Hotkeys", "Load State From Slot 6")) DEFINE_HOTKEY_SAVESTATE_X(7, TRANSLATE_NOOP("Hotkeys", "Save State To Slot 7")) DEFINE_HOTKEY_LOADSTATE_X(7, TRANSLATE_NOOP("Hotkeys", "Load State From Slot 7")) DEFINE_HOTKEY_SAVESTATE_X(8, TRANSLATE_NOOP("Hotkeys", "Save State To Slot 8")) DEFINE_HOTKEY_LOADSTATE_X(8, TRANSLATE_NOOP("Hotkeys", "Load State From Slot 8")) DEFINE_HOTKEY_SAVESTATE_X(9, TRANSLATE_NOOP("Hotkeys", "Save State To Slot 9")) DEFINE_HOTKEY_LOADSTATE_X(9, TRANSLATE_NOOP("Hotkeys", "Load State From Slot 9")) DEFINE_HOTKEY_SAVESTATE_X(10, TRANSLATE_NOOP("Hotkeys", "Save State To Slot 10")) DEFINE_HOTKEY_LOADSTATE_X(10, TRANSLATE_NOOP("Hotkeys", "Load State From Slot 10")) #undef DEFINE_HOTKEY_SAVESTATE_X #undef DEFINE_HOTKEY_LOADSTATE_X DEFINE_HOTKEY("Mute", TRANSLATE_NOOP("Hotkeys", "Audio"), TRANSLATE_NOOP("Hotkeys", "Toggle Mute"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) HotkeyToggleMute(); }) DEFINE_HOTKEY("IncreaseVolume", TRANSLATE_NOOP("Hotkeys", "Audio"), TRANSLATE_NOOP("Hotkeys", "Increase Volume"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) HotkeyAdjustVolume(5); }) DEFINE_HOTKEY("DecreaseVolume", TRANSLATE_NOOP("Hotkeys", "Audio"), TRANSLATE_NOOP("Hotkeys", "Decrease Volume"), [](s32 pressed) { if (!pressed && VMManager::HasValidVM()) HotkeyAdjustVolume(-5); }) END_HOTKEY_LIST()