Qt: Implement save-state-on-shutdown

This commit is contained in:
Connor McLaughlin 2022-05-07 22:56:44 +10:00 committed by refractionpcsx2
parent 935dd046da
commit c21d475bbd
14 changed files with 182 additions and 76 deletions

View File

@ -109,6 +109,7 @@ void EmuThread::startVM(std::shared_ptr<VMBootParameters> boot_params)
m_is_fullscreen = boot_params->fullscreen.value_or(QtHost::GetBaseBoolSettingValue("UI", "StartFullscreen", false));
m_is_rendering_to_main = QtHost::GetBaseBoolSettingValue("UI", "RenderToMainWindow", true);
m_is_surfaceless = false;
m_save_state_on_shutdown = false;
if (!VMManager::Initialize(*boot_params))
return;
@ -142,14 +143,21 @@ void EmuThread::setVMPaused(bool paused)
VMManager::SetPaused(paused);
}
void EmuThread::shutdownVM(bool allow_save_to_state /* = true */)
void EmuThread::shutdownVM(bool save_state /* = true */)
{
if (!isOnEmuThread())
{
QMetaObject::invokeMethod(this, "shutdownVM", Qt::QueuedConnection, Q_ARG(bool, save_state));
return;
}
const VMState state = VMManager::GetState();
if (state == VMState::Paused)
m_event_loop->quit();
else if (state != VMState::Running)
return;
m_save_state_on_shutdown = save_state;
VMManager::SetState(VMState::Stopping);
}
@ -254,7 +262,7 @@ void EmuThread::destroyVM()
m_last_video_fps = 0.0f;
m_last_internal_width = 0;
m_last_internal_height = 0;
VMManager::Shutdown();
VMManager::Shutdown(m_save_state_on_shutdown);
}
void EmuThread::executeVM()

View File

@ -59,7 +59,7 @@ public Q_SLOTS:
void startVM(std::shared_ptr<VMBootParameters> boot_params);
void resetVM();
void setVMPaused(bool paused);
void shutdownVM(bool allow_save_to_state = true);
void shutdownVM(bool save_state = true);
void loadState(const QString& filename);
void loadStateFromSlot(qint32 slot);
void saveState(const QString& filename);
@ -159,6 +159,7 @@ private:
bool m_is_rendering_to_main = false;
bool m_is_fullscreen = false;
bool m_is_surfaceless = false;
bool m_save_state_on_shutdown = false;
float m_last_speed = 0.0f;
float m_last_game_fps = 0.0f;

View File

@ -143,7 +143,8 @@ void MainWindow::connectSignals()
connect(m_ui.actionChangeDiscFromGameList, &QAction::triggered, this, &MainWindow::onChangeDiscFromGameListActionTriggered);
connect(m_ui.menuChangeDisc, &QMenu::aboutToShow, this, &MainWindow::onChangeDiscMenuAboutToShow);
connect(m_ui.menuChangeDisc, &QMenu::aboutToHide, this, &MainWindow::onChangeDiscMenuAboutToHide);
connect(m_ui.actionPowerOff, &QAction::triggered, this, [this]() { requestShutdown(); });
connect(m_ui.actionPowerOff, &QAction::triggered, this, [this]() { requestShutdown(true, true); });
connect(m_ui.actionPowerOffWithoutSaving, &QAction::triggered, this, [this]() { requestShutdown(false, false); });
connect(m_ui.actionLoadState, &QAction::triggered, this, [this]() { m_ui.menuLoadState->exec(QCursor::pos()); });
connect(m_ui.actionSaveState, &QAction::triggered, this, [this]() { m_ui.menuSaveState->exec(QCursor::pos()); });
connect(m_ui.actionExit, &QAction::triggered, this, &MainWindow::close);
@ -562,6 +563,7 @@ void MainWindow::updateEmulationActions(bool starting, bool running)
m_ui.actionStartBios->setDisabled(starting_or_running);
m_ui.actionPowerOff->setEnabled(running);
m_ui.actionPowerOffWithoutSaving->setEnabled(running);
m_ui.actionReset->setEnabled(running);
m_ui.actionPause->setEnabled(running);
m_ui.actionChangeDisc->setEnabled(running);
@ -739,18 +741,34 @@ bool MainWindow::requestShutdown(bool allow_confirm /* = true */, bool allow_sav
if (!VMManager::HasValidVM())
return true;
// if we don't have a crc, we can't save state
allow_save_to_state &= (m_current_game_crc != 0);
bool save_state = allow_save_to_state && EmuConfig.SaveStateOnShutdown;
// only confirm on UI thread because we need to display a msgbox
if (allow_confirm && !GSDumpReplayer::IsReplayingDump() && QtHost::GetBaseBoolSettingValue("UI", "ConfirmShutdown", true))
{
VMLock lock(pauseAndLockVM());
if (QMessageBox::question(lock.getDialogParent(), tr("Confirm Shutdown"),
tr("Are you sure you want to shut down the virtual machine?\n\nAll unsaved progress will be lost.")) != QMessageBox::Yes)
{
QMessageBox msgbox(lock.getDialogParent());
msgbox.setIcon(QMessageBox::Question);
msgbox.setWindowTitle(tr("Confirm Shutdown"));
msgbox.setText("Are you sure you want to shut down the virtual machine?");
QCheckBox* save_cb = new QCheckBox(tr("Save State For Resume"), &msgbox);
save_cb->setChecked(save_state);
save_cb->setEnabled(allow_save_to_state);
msgbox.setCheckBox(save_cb);
msgbox.addButton(QMessageBox::Yes);
msgbox.addButton(QMessageBox::No);
msgbox.setDefaultButton(QMessageBox::Yes);
if (msgbox.exec() != QMessageBox::Yes)
return false;
}
save_state = save_cb->isChecked();
}
g_emu_thread->shutdownVM(allow_save_to_state);
g_emu_thread->shutdownVM(save_state);
if (block_until_done || QtHost::InBatchMode())
{
@ -817,9 +835,16 @@ void MainWindow::onGameListEntryActivated()
return;
}
const std::optional<bool> resume = promptForResumeState(
QString::fromStdString(VMManager::GetSaveStateFileName(entry->serial.c_str(), entry->crc, -1)));
if (!resume.has_value())
{
// cancelled
return;
}
// only resume if the option is enabled, and we have one for this game
const bool resume = (VMManager::ShouldSaveResumeState() && VMManager::HasSaveStateInSlot(entry->serial.c_str(), entry->crc, -1));
startGameListEntry(entry, resume ? std::optional<s32>(-1) : std::optional<s32>(), std::nullopt);
startGameListEntry(entry, resume.value() ? std::optional<s32>(-1) : std::optional<s32>(), std::nullopt);
}
void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point)
@ -856,7 +881,7 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point)
connect(action, &QAction::triggered, [this, entry]() { startGameListEntry(entry); });
// Make bold to indicate it's the default choice when double-clicking
if (!VMManager::ShouldSaveResumeState() || !VMManager::HasSaveStateInSlot(entry->serial.c_str(), entry->crc, -1))
if (!VMManager::HasSaveStateInSlot(entry->serial.c_str(), entry->crc, -1))
QtUtils::MarkActionAsDefault(action);
action = menu.addAction(tr("Fast Boot"));
@ -902,6 +927,15 @@ void MainWindow::onStartFileActionTriggered()
std::shared_ptr<VMBootParameters> params = std::make_shared<VMBootParameters>();
params->filename = filename.toStdString();
const std::optional<bool> resume(
promptForResumeState(
QString::fromStdString(VMManager::GetSaveStateFileName(params->filename.c_str(), -1))));
if (!resume.has_value())
return;
else if (resume.value())
params->state_index = -1;
g_emu_thread->startVM(std::move(params));
}
@ -1582,6 +1616,49 @@ void MainWindow::setGameListEntryCoverImage(const GameList::Entry* entry)
m_game_list_widget->refreshGridCovers();
}
std::optional<bool> MainWindow::promptForResumeState(const QString& save_state_path)
{
if (save_state_path.isEmpty())
return false;
QFileInfo fi(save_state_path);
if (!fi.exists())
return false;
QMessageBox msgbox(this);
msgbox.setIcon(QMessageBox::Question);
msgbox.setWindowTitle(tr("Load Resume State"));
msgbox.setText(
tr("A resume save state was found for this game, saved at:\n\n%1.\n\nDo you want to load this state, or start from a fresh boot?")
.arg(fi.lastModified().toLocalTime().toString()));
QPushButton* load = msgbox.addButton(tr("Load State"), QMessageBox::AcceptRole);
QPushButton* boot = msgbox.addButton(tr("Fresh Boot"), QMessageBox::RejectRole);
QPushButton* delboot = msgbox.addButton(tr("Delete And Boot"), QMessageBox::RejectRole);
QPushButton* cancel = msgbox.addButton(QMessageBox::Cancel);
msgbox.setDefaultButton(load);
msgbox.exec();
QAbstractButton* clicked = msgbox.clickedButton();
if (load == clicked)
{
return true;
}
else if (boot == clicked)
{
return false;
}
else if (delboot == clicked)
{
if (!QFile::remove(save_state_path))
QMessageBox::critical(this, tr("Error"), tr("Failed to delete save state file '%1'.").arg(save_state_path));
return false;
}
return std::nullopt;
}
void MainWindow::loadSaveStateSlot(s32 slot)
{
if (m_vm_valid)
@ -1659,8 +1736,7 @@ void MainWindow::populateLoadStateMenu(QMenu* menu, const QString& filename, con
connect(action, &QAction::triggered, [this]() { loadSaveStateSlot(-1); });
// Make bold to indicate it's the default choice when double-clicking
if (VMManager::ShouldSaveResumeState())
QtUtils::MarkActionAsDefault(action);
QtUtils::MarkActionAsDefault(action);
}
}

View File

@ -185,6 +185,7 @@ private:
std::optional<bool> fast_boot = std::nullopt);
void setGameListEntryCoverImage(const GameList::Entry* entry);
std::optional<bool> promptForResumeState(const QString& save_state_path);
void loadSaveStateSlot(s32 slot);
void loadSaveStateFile(const QString& filename, const QString& state_filename);
void populateLoadStateMenu(QMenu* menu, const QString& filename, const QString& serial, quint32 crc);

View File

@ -80,6 +80,7 @@
<addaction name="actionStartBios"/>
<addaction name="separator"/>
<addaction name="actionPowerOff"/>
<addaction name="actionPowerOffWithoutSaving"/>
<addaction name="actionReset"/>
<addaction name="actionPause"/>
<addaction name="menuChangeDisc"/>
@ -285,6 +286,15 @@
<string>Shut &amp;Down</string>
</property>
</action>
<action name="actionPowerOffWithoutSaving">
<property name="icon">
<iconset theme="close-line">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>Shut Down &amp;Without Saving</string>
</property>
</action>
<action name="actionReset">
<property name="icon">
<iconset theme="restart-line">

View File

@ -42,7 +42,7 @@ InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsDialog* dialog, QWidget
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.inhibitScreensaver, "UI", "InhibitScreensaver", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.discordPresence, "UI", "DiscordPresence", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.confirmShutdown, "UI", "ConfirmShutdown", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.saveStateOnExit, "EmuCore", "AutoStateLoadSave", false);
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);
@ -87,7 +87,7 @@ InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsDialog* dialog, QWidget
m_ui.confirmShutdown, tr("Confirm Shutdown"), tr("Checked"),
tr("Determines whether a prompt will be displayed to confirm shutting down the virtual machine "
"when the hotkey is pressed."));
dialog->registerWidgetHelp(m_ui.saveStateOnExit, tr("Save State On Exit"), tr("Checked"),
dialog->registerWidgetHelp(m_ui.saveStateOnShutdown, tr("Save State On Shutdown"), tr("Checked"),
tr("Automatically saves the emulator state when powering down or exiting. You can then "
"resume directly from where you left off next time."));
dialog->registerWidgetHelp(m_ui.pauseOnStart, tr("Pause On Start"), tr("Unchecked"),

View File

@ -68,9 +68,9 @@
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="saveStateOnExit">
<widget class="QCheckBox" name="saveStateOnShutdown">
<property name="text">
<string>Save Or Load State On Exit / Resume</string>
<string>Save State On Shutdown</string>
</property>
</widget>
</item>

View File

@ -939,6 +939,7 @@ struct Pcsx2Config
#endif
#ifdef PCSX2_CORE
EnableGameFixes : 1, // enables automatic game fixes
SaveStateOnShutdown : 1, // default value for saving state on shutdown
#endif
// when enabled uses BOOT2 injection, skipping sony bios splashes
UseBOOT2Injection : 1,

View File

@ -1062,6 +1062,7 @@ void Pcsx2Config::LoadSave(SettingsWrapper& wrap)
#endif
#ifdef PCSX2_CORE
SettingsWrapBitBool(EnableGameFixes);
SettingsWrapBitBool(SaveStateOnShutdown);
#endif
SettingsWrapBitBool(ConsoleToStdio);
SettingsWrapBitBool(HostFs);

View File

@ -783,8 +783,7 @@ static bool SaveState_CompressScreenshot(SaveStateScreenshotData* data, zip_t* z
// --------------------------------------------------------------------------------------
// CompressThread_VmState
// --------------------------------------------------------------------------------------
static void ZipStateToDiskOnThread(std::unique_ptr<ArchiveEntryList> srclist, std::unique_ptr<SaveStateScreenshotData> screenshot,
std::string filename, s32 slot_for_message)
void SaveState_ZipToDisk(std::unique_ptr<ArchiveEntryList> srclist, std::unique_ptr<SaveStateScreenshotData> screenshot, std::string filename, s32 slot_for_message)
{
#ifndef PCSX2_CORE
wxGetApp().StartPendingSave();
@ -858,9 +857,10 @@ static void ZipStateToDiskOnThread(std::unique_ptr<ArchiveEntryList> srclist, st
#endif
}
void SaveState_ZipToDisk(std::unique_ptr<ArchiveEntryList> srclist, std::unique_ptr<SaveStateScreenshotData> screenshot, std::string filename, s32 slot_for_message)
void SaveState_ZipToDiskOnThread(std::unique_ptr<ArchiveEntryList> srclist, std::unique_ptr<SaveStateScreenshotData> screenshot, std::string filename, s32 slot_for_message)
{
std::thread threaded_save(ZipStateToDiskOnThread, std::move(srclist), std::move(screenshot), std::move(filename), slot_for_message);
std::thread threaded_save(SaveState_ZipToDisk, std::move(srclist), std::move(screenshot), std::move(filename), slot_for_message);
threaded_save.detach();
}

View File

@ -59,6 +59,7 @@ class ArchiveEntryList;
extern std::unique_ptr<ArchiveEntryList> SaveState_DownloadState();
extern std::unique_ptr<SaveStateScreenshotData> SaveState_SaveScreenshot();
extern void SaveState_ZipToDisk(std::unique_ptr<ArchiveEntryList> srclist, std::unique_ptr<SaveStateScreenshotData> screenshot, std::string filename, s32 slot_for_message);
extern void SaveState_ZipToDiskOnThread(std::unique_ptr<ArchiveEntryList> srclist, std::unique_ptr<SaveStateScreenshotData> screenshot, std::string filename, s32 slot_for_message);
extern void SaveState_UnzipFromDisk(const std::string& filename);
// --------------------------------------------------------------------------------------

View File

@ -84,7 +84,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);
static bool DoSaveState(const char* filename, s32 slot_for_message, bool save_on_thread);
static void SetTimerResolutionIncreased(bool enabled);
static void SetEmuThreadAffinities(bool force);
@ -534,30 +534,11 @@ bool VMManager::ApplyBootParameters(const VMBootParameters& params, std::string*
return false;
}
// try the game list first, but this won't work if we're in batch mode
*state_to_load = GetSaveStateFileName(params.filename.c_str(), params.state_index.value());
if (state_to_load->empty())
{
auto lock = GameList::GetLock();
if (const GameList::Entry* entry = GameList::GetEntryForPath(params.filename.c_str()); entry)
{
*state_to_load = GetSaveStateFileName(entry->serial.c_str(), entry->crc, params.state_index.value());
}
else
{
// just scan it.. hopefully it'll come back okay
GameList::Entry temp_entry;
if (!GameList::PopulateEntryFromPath(params.filename.c_str(), &temp_entry))
{
Host::ReportFormattedErrorAsync("Error", "Could not scan path '%s' for indexed save state load.", params.filename.c_str());
return false;
}
*state_to_load = GetSaveStateFileName(temp_entry.serial.c_str(), temp_entry.crc, params.state_index.value());
}
if (state_to_load->empty())
{
Host::ReportFormattedErrorAsync("Error", "Could not resolve path indexed save state load.");
return false;
}
Host::ReportFormattedErrorAsync("Error", "Could not resolve path indexed save state load.");
return false;
}
}
@ -771,7 +752,7 @@ bool VMManager::Initialize(const VMBootParameters& boot_params)
{
if (!DoLoadState(state_to_load.c_str()))
{
Shutdown();
Shutdown(false);
return false;
}
}
@ -779,7 +760,7 @@ bool VMManager::Initialize(const VMBootParameters& boot_params)
return true;
}
void VMManager::Shutdown(bool allow_save_resume_state /* = true */)
void VMManager::Shutdown(bool save_resume_state)
{
SetTimerResolutionIncreased(false);
@ -788,10 +769,10 @@ void VMManager::Shutdown(bool allow_save_resume_state /* = true */)
vu1Thread.WaitVU();
GetMTGS().WaitGS();
if (!GSDumpReplayer::IsReplayingDump() && allow_save_resume_state && ShouldSaveResumeState())
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))
if (!resume_file_name.empty() && !DoSaveState(resume_file_name.c_str(), -1, false))
Console.Error("Failed to save resume state");
}
else if (GSDumpReplayer::IsReplayingDump())
@ -854,23 +835,45 @@ void VMManager::Reset()
UpdateRunningGame(true);
}
bool VMManager::ShouldSaveResumeState()
{
return Host::GetBoolSettingValue("EmuCore", "AutoStateLoadSave", false);
}
std::string VMManager::GetSaveStateFileName(const char* game_serial, u32 game_crc, s32 slot)
{
if (!game_serial || game_serial[0] == '\0')
return std::string();
std::string filename;
if (slot < 0)
filename = StringUtil::StdStringFromFormat("%s (%08X).resume.p2s", game_serial, game_crc);
else
filename = StringUtil::StdStringFromFormat("%s (%08X).%02d.p2s", game_serial, game_crc, slot);
if (game_crc != 0)
{
if (slot < 0)
filename = StringUtil::StdStringFromFormat("%s (%08X).resume.p2s", game_serial, game_crc);
else
filename = StringUtil::StdStringFromFormat("%s (%08X).%02d.p2s", game_serial, game_crc, slot);
return Path::CombineStdString(EmuFolders::Savestates, filename);
filename = Path::CombineStdString(EmuFolders::Savestates, filename);
}
return filename;
}
std::string VMManager::GetSaveStateFileName(const char* filename, s32 slot)
{
pxAssertRel(!HasValidVM(), "Should not have a VM when calling the non-gamelist GetSaveStateFileName()");
std::string ret;
// try the game list first, but this won't work if we're in batch mode
auto lock = GameList::GetLock();
if (const GameList::Entry* entry = GameList::GetEntryForPath(filename); entry)
{
ret = GetSaveStateFileName(entry->serial.c_str(), entry->crc, slot);
}
else
{
// just scan it.. hopefully it'll come back okay
GameList::Entry temp_entry;
if (GameList::PopulateEntryFromPath(filename, &temp_entry))
{
ret = GetSaveStateFileName(temp_entry.serial.c_str(), temp_entry.crc, slot);
}
}
return ret;
}
bool VMManager::HasSaveStateInSlot(const char* game_serial, u32 game_crc, s32 slot)
@ -906,7 +909,7 @@ bool VMManager::DoLoadState(const char* filename)
}
}
bool VMManager::DoSaveState(const char* filename, s32 slot_for_message)
bool VMManager::DoSaveState(const char* filename, s32 slot_for_message, bool zip_on_thread)
{
if (GSDumpReplayer::IsReplayingDump())
return false;
@ -914,7 +917,11 @@ bool VMManager::DoSaveState(const char* filename, s32 slot_for_message)
try
{
std::unique_ptr<ArchiveEntryList> elist = SaveState_DownloadState();
SaveState_ZipToDisk(std::move(elist), SaveState_SaveScreenshot(), filename, slot_for_message);
if (zip_on_thread)
SaveState_ZipToDiskOnThread(std::move(elist), SaveState_SaveScreenshot(), filename, slot_for_message);
else
SaveState_ZipToDisk(std::move(elist), SaveState_SaveScreenshot(), filename, slot_for_message);
Host::InvalidateSaveStateCache();
Host::OnSaveStateSaved(filename);
return true;
@ -949,12 +956,12 @@ bool VMManager::LoadStateFromSlot(s32 slot)
return DoLoadState(filename.c_str());
}
bool VMManager::SaveState(const char* filename)
bool VMManager::SaveState(const char* filename, bool zip_on_thread)
{
return DoSaveState(filename, -1);
return DoSaveState(filename, -1, zip_on_thread);
}
bool VMManager::SaveStateToSlot(s32 slot)
bool VMManager::SaveStateToSlot(s32 slot, bool zip_on_thread)
{
const std::string filename(GetCurrentSaveStateFileName(slot));
if (filename.empty())
@ -962,7 +969,7 @@ bool VMManager::SaveStateToSlot(s32 slot)
// if it takes more than a minute.. well.. wtf.
Host::AddKeyedFormattedOSDMessage(StringUtil::StdStringFromFormat("SaveStateSlot%d", slot), 60.0f, "Saving state to slot %d...", slot);
return DoSaveState(filename.c_str(), slot);
return DoSaveState(filename.c_str(), slot, zip_on_thread);
}
LimiterModeType VMManager::GetLimiterMode()

View File

@ -76,7 +76,7 @@ namespace VMManager
bool Initialize(const VMBootParameters& boot_params);
/// Destroys all system components.
void Shutdown(bool allow_save_resume_state = true);
void Shutdown(bool save_resume_state);
/// Resets all subsystems to a cold boot.
void Reset();
@ -96,12 +96,12 @@ namespace VMManager
/// Reloads cheats/patches. If verbose is set, the number of patches loaded will be shown in the OSD.
void ReloadPatches(bool verbose);
/// Returns true if a resume save state should be saved/loaded.
bool ShouldSaveResumeState();
/// Returns the save state filename for the given game serial/crc.
std::string GetSaveStateFileName(const char* game_serial, u32 game_crc, s32 slot);
/// Returns the path to save state for the specified disc/elf.
std::string GetSaveStateFileName(const char* filename, s32 slot);
/// Returns true if there is a save state in the specified slot.
bool HasSaveStateInSlot(const char* game_serial, u32 game_crc, s32 slot);
@ -112,10 +112,10 @@ namespace VMManager
bool LoadStateFromSlot(s32 slot);
/// Saves state to the specified filename.
bool SaveState(const char* filename);
bool SaveState(const char* filename, bool zip_on_thread = true);
/// Saves state to the specified slot.
bool SaveStateToSlot(s32 slot);
bool SaveStateToSlot(s32 slot, bool zip_on_thread = true);
/// Returns the current limiter mode.
LimiterModeType GetLimiterMode();

View File

@ -61,7 +61,7 @@ protected:
std::unique_ptr<ArchiveEntryList> elist = SaveState_DownloadState();
UI_EnableStateActions();
paused_core.AllowResume();
SaveState_ZipToDisk(std::move(elist), nullptr, StringUtil::wxStringToUTF8String(m_filename), -1);
SaveState_ZipToDiskOnThread(std::move(elist), nullptr, StringUtil::wxStringToUTF8String(m_filename), -1);
}
};