diff --git a/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp b/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp
index a3e110d554..276e4d3f40 100644
--- a/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp
+++ b/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp
@@ -59,6 +59,7 @@ InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsDialog* dialog, QWidget
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.saveStateOnShutdown, "EmuCore", "SaveStateOnShutdown", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.pauseOnStart, "UI", "StartPaused", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.pauseOnFocusLoss, "UI", "PauseOnFocusLoss", false);
+ SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.backupSaveStates, "EmuCore", "BackupSavestate", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.startFullscreen, "UI", "StartFullscreen", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.doubleClickTogglesFullscreen, "UI", "DoubleClickTogglesFullscreen",
@@ -115,6 +116,8 @@ InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsDialog* dialog, QWidget
dialog->registerWidgetHelp(m_ui.pauseOnFocusLoss, tr("Pause On Focus Loss"), tr("Unchecked"),
tr("Pauses the emulator when you minimize the window or switch to another application, "
"and unpauses when you switch back."));
+ dialog->registerWidgetHelp(m_ui.backupSaveStates, tr("Create Save State Backups"), tr("Unchecked"),
+ tr("Creates a backup copy of a save state if it already exists when the save is created. The backup copy has a .backup suffix."));
dialog->registerWidgetHelp(m_ui.startFullscreen, tr("Start Fullscreen"), tr("Unchecked"),
tr("Automatically switches to fullscreen mode when a game is started."));
dialog->registerWidgetHelp(m_ui.hideMouseCursor, tr("Hide Cursor In Fullscreen"), tr("Checked"),
diff --git a/pcsx2-qt/Settings/InterfaceSettingsWidget.ui b/pcsx2-qt/Settings/InterfaceSettingsWidget.ui
index 4255c806dd..b91fc0794a 100644
--- a/pcsx2-qt/Settings/InterfaceSettingsWidget.ui
+++ b/pcsx2-qt/Settings/InterfaceSettingsWidget.ui
@@ -32,31 +32,17 @@
Behaviour
- -
-
-
- Inhibit Screensaver
-
-
-
- -
-
-
- Pause On Start
-
-
-
- -
+
-
Pause On Focus Loss
- -
-
+
-
+
- Confirm Shutdown
+ Inhibit Screensaver
@@ -67,7 +53,28 @@
- -
+
-
+
+
+ Pause On Start
+
+
+
+ -
+
+
+ Confirm Shutdown
+
+
+
+ -
+
+
+ Create Save State Backups
+
+
+
+ -
Enable Discord Presence
diff --git a/pcsx2/VMManager.cpp b/pcsx2/VMManager.cpp
index d72ec75444..98fc2652d8 100644
--- a/pcsx2/VMManager.cpp
+++ b/pcsx2/VMManager.cpp
@@ -93,7 +93,7 @@ namespace VMManager
static std::string GetCurrentSaveStateFileName(s32 slot);
static bool DoLoadState(const char* filename);
- static bool DoSaveState(const char* filename, s32 slot_for_message, bool zip_on_thread);
+ static bool DoSaveState(const char* filename, s32 slot_for_message, bool zip_on_thread, bool backup_old_state);
static void ZipSaveState(std::unique_ptr elist,
std::unique_ptr screenshot, std::string osd_key,
const char* filename, s32 slot_for_message);
@@ -1028,7 +1028,7 @@ void VMManager::Shutdown(bool save_resume_state)
if (!GSDumpReplayer::IsReplayingDump() && save_resume_state)
{
std::string resume_file_name(GetCurrentSaveStateFileName(-1));
- if (!resume_file_name.empty() && !DoSaveState(resume_file_name.c_str(), -1, true))
+ if (!resume_file_name.empty() && !DoSaveState(resume_file_name.c_str(), -1, true, false))
Console.Error("Failed to save resume state");
}
else if (GSDumpReplayer::IsReplayingDump())
@@ -1185,7 +1185,7 @@ bool VMManager::DoLoadState(const char* filename)
}
}
-bool VMManager::DoSaveState(const char* filename, s32 slot_for_message, bool zip_on_thread)
+bool VMManager::DoSaveState(const char* filename, s32 slot_for_message, bool zip_on_thread, bool backup_old_state)
{
if (GSDumpReplayer::IsReplayingDump())
return false;
@@ -1197,6 +1197,17 @@ bool VMManager::DoSaveState(const char* filename, s32 slot_for_message, bool zip
std::unique_ptr elist(SaveState_DownloadState());
std::unique_ptr screenshot(SaveState_SaveScreenshot());
+ if (FileSystem::FileExists(filename) && backup_old_state)
+ {
+ const std::string backup_filename(fmt::format("{}.backup", filename));
+ Console.WriteLn(fmt::format("Creating save state backup {}...", backup_filename));
+ if (!FileSystem::RenamePath(filename, backup_filename.c_str()))
+ {
+ Host::AddIconOSDMessage(std::move(osd_key), ICON_FA_EXCLAMATION_TRIANGLE,
+ fmt::format("Failed to back up old save state {}.", Path::GetFileName(filename)));
+ }
+ }
+
if (zip_on_thread)
{
// lock order here is important; the thread could exit before we resume here.
@@ -1336,9 +1347,9 @@ bool VMManager::LoadStateFromSlot(s32 slot)
return DoLoadState(filename.c_str());
}
-bool VMManager::SaveState(const char* filename, bool zip_on_thread)
+bool VMManager::SaveState(const char* filename, bool zip_on_thread, bool backup_old_state)
{
- return DoSaveState(filename, -1, zip_on_thread);
+ return DoSaveState(filename, -1, zip_on_thread, backup_old_state);
}
bool VMManager::SaveStateToSlot(s32 slot, bool zip_on_thread)
@@ -1349,7 +1360,7 @@ bool VMManager::SaveStateToSlot(s32 slot, bool zip_on_thread)
// if it takes more than a minute.. well.. wtf.
Host::AddIconOSDMessage(fmt::format("SaveStateSlot{}", slot), ICON_FA_SAVE, fmt::format("Saving state to slot {}...", slot), 60.0f);
- return DoSaveState(filename.c_str(), slot, zip_on_thread);
+ return DoSaveState(filename.c_str(), slot, zip_on_thread, EmuConfig.BackupSavestate);
}
LimiterModeType VMManager::GetLimiterMode()
diff --git a/pcsx2/VMManager.h b/pcsx2/VMManager.h
index 4e4f180703..e5af10e359 100644
--- a/pcsx2/VMManager.h
+++ b/pcsx2/VMManager.h
@@ -122,7 +122,7 @@ namespace VMManager
bool LoadStateFromSlot(s32 slot);
/// Saves state to the specified filename.
- bool SaveState(const char* filename, bool zip_on_thread = true);
+ bool SaveState(const char* filename, bool zip_on_thread = true, bool backup_old_state = false);
/// Saves state to the specified slot.
bool SaveStateToSlot(s32 slot, bool zip_on_thread = true);