core, citra_qt: Implement a save states file format and slot UI

10 slots are offered along with 'Save to Oldest Slot' and 'Load from Newest Slot'.

The savestate format is similar to the movie file format. It is called CST (Citra SavesTate), and is basically a 0x100 byte header (consisting of magic, revision, creation time and title ID) followed by Zstd compressed raw savestate data.

The savestate files are saved to the `states` folder in Citra's user folder. The files are named like `<Title ID>.<Slot ID>.cst`.
This commit is contained in:
zhupengfei
2020-02-18 13:19:52 +08:00
parent 7d880f94db
commit a487016cb4
11 changed files with 384 additions and 77 deletions

View File

@@ -79,6 +79,7 @@
#include "core/hle/service/nfc/nfc.h"
#include "core/loader/loader.h"
#include "core/movie.h"
#include "core/savestate.h"
#include "core/settings.h"
#include "game_list_p.h"
#include "video_core/renderer_base.h"
@@ -166,6 +167,7 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) {
InitializeWidgets();
InitializeDebugWidgets();
InitializeRecentFileMenuActions();
InitializeSaveStateMenuActions();
InitializeHotkeys();
ShowUpdaterWidgets();
@@ -383,6 +385,32 @@ void GMainWindow::InitializeRecentFileMenuActions() {
UpdateRecentFiles();
}
void GMainWindow::InitializeSaveStateMenuActions() {
for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) {
actions_load_state[i] = new QAction(this);
actions_load_state[i]->setData(i + 1);
connect(actions_load_state[i], &QAction::triggered, this, &GMainWindow::OnLoadState);
ui.menu_Load_State->addAction(actions_load_state[i]);
actions_save_state[i] = new QAction(this);
actions_save_state[i]->setData(i + 1);
connect(actions_save_state[i], &QAction::triggered, this, &GMainWindow::OnSaveState);
ui.menu_Save_State->addAction(actions_save_state[i]);
}
connect(ui.action_Load_from_Newest_Slot, &QAction::triggered,
[this] { actions_load_state[newest_slot - 1]->trigger(); });
connect(ui.action_Save_to_Oldest_Slot, &QAction::triggered,
[this] { actions_save_state[oldest_slot - 1]->trigger(); });
connect(ui.menu_Load_State->menuAction(), &QAction::hovered, this,
&GMainWindow::UpdateSaveStates);
connect(ui.menu_Save_State->menuAction(), &QAction::hovered, this,
&GMainWindow::UpdateSaveStates);
UpdateSaveStates();
}
void GMainWindow::InitializeHotkeys() {
hotkey_registry.LoadHotkeys();
@@ -607,8 +635,6 @@ void GMainWindow::ConnectMenuEvents() {
&GMainWindow::OnMenuReportCompatibility);
connect(ui.action_Configure, &QAction::triggered, this, &GMainWindow::OnConfigure);
connect(ui.action_Cheats, &QAction::triggered, this, &GMainWindow::OnCheats);
connect(ui.action_Save, &QAction::triggered, this, &GMainWindow::OnSave);
connect(ui.action_Load, &QAction::triggered, this, &GMainWindow::OnLoad);
// View
connect(ui.action_Single_Window_Mode, &QAction::triggered, this,
@@ -1036,8 +1062,6 @@ void GMainWindow::ShutdownGame() {
ui.action_Stop->setEnabled(false);
ui.action_Restart->setEnabled(false);
ui.action_Cheats->setEnabled(false);
ui.action_Save->setEnabled(false);
ui.action_Load->setEnabled(false);
ui.action_Load_Amiibo->setEnabled(false);
ui.action_Remove_Amiibo->setEnabled(false);
ui.action_Report_Compatibility->setEnabled(false);
@@ -1061,6 +1085,8 @@ void GMainWindow::ShutdownGame() {
game_fps_label->setVisible(false);
emu_frametime_label->setVisible(false);
UpdateSaveStates();
emulation_running = false;
if (defer_update_prompt) {
@@ -1107,6 +1133,62 @@ void GMainWindow::UpdateRecentFiles() {
ui.menu_recent_files->setEnabled(num_recent_files != 0);
}
void GMainWindow::UpdateSaveStates() {
if (!Core::System::GetInstance().IsPoweredOn()) {
ui.menu_Load_State->setEnabled(false);
ui.menu_Save_State->setEnabled(false);
return;
}
ui.menu_Load_State->setEnabled(true);
ui.menu_Save_State->setEnabled(true);
ui.action_Load_from_Newest_Slot->setEnabled(false);
oldest_slot = newest_slot = 0;
oldest_slot_time = std::numeric_limits<u64>::max();
newest_slot_time = 0;
u64 title_id;
if (Core::System::GetInstance().GetAppLoader().ReadProgramId(title_id) !=
Loader::ResultStatus::Success) {
return;
}
auto savestates = Core::ListSaveStates(title_id);
for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) {
actions_load_state[i]->setEnabled(false);
actions_load_state[i]->setText(tr("Slot %1").arg(i + 1));
actions_save_state[i]->setText(tr("Slot %1").arg(i + 1));
}
for (const auto& savestate : savestates) {
const auto text = tr("Slot %1 - %2")
.arg(savestate.slot)
.arg(QDateTime::fromSecsSinceEpoch(savestate.time)
.toString(QStringLiteral("yyyy-MM-dd hh:mm:ss")));
actions_load_state[savestate.slot - 1]->setEnabled(true);
actions_load_state[savestate.slot - 1]->setText(text);
actions_save_state[savestate.slot - 1]->setText(text);
ui.action_Load_from_Newest_Slot->setEnabled(true);
if (savestate.time > newest_slot_time) {
newest_slot = savestate.slot;
newest_slot_time = savestate.time;
}
if (savestate.time < oldest_slot_time) {
oldest_slot = savestate.slot;
oldest_slot_time = savestate.time;
}
}
for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) {
if (!actions_load_state[i]->isEnabled()) {
// Prefer empty slot
oldest_slot = i + 1;
oldest_slot_time = 0;
break;
}
}
}
void GMainWindow::OnGameListLoadFile(QString game_path) {
BootGame(game_path);
}
@@ -1348,14 +1430,14 @@ void GMainWindow::OnStartGame() {
ui.action_Stop->setEnabled(true);
ui.action_Restart->setEnabled(true);
ui.action_Cheats->setEnabled(true);
ui.action_Save->setEnabled(true);
ui.action_Load->setEnabled(true);
ui.action_Load_Amiibo->setEnabled(true);
ui.action_Report_Compatibility->setEnabled(true);
ui.action_Enable_Frame_Advancing->setEnabled(true);
ui.action_Capture_Screenshot->setEnabled(true);
discord_rpc->Update();
UpdateSaveStates();
}
void GMainWindow::OnPauseGame() {
@@ -1503,14 +1585,19 @@ void GMainWindow::OnCheats() {
cheat_dialog.exec();
}
void GMainWindow::OnSave() {
Core::System::GetInstance().SendSignal(Core::System::Signal::Save);
void GMainWindow::OnSaveState() {
QAction* action = qobject_cast<QAction*>(sender());
assert(action);
Core::System::GetInstance().SendSignal(Core::System::Signal::Save, action->data().toUInt());
UpdateSaveStates();
}
void GMainWindow::OnLoad() {
if (QFileInfo("save0.citrasave").exists()) {
Core::System::GetInstance().SendSignal(Core::System::Signal::Load);
}
void GMainWindow::OnLoadState() {
QAction* action = qobject_cast<QAction*>(sender());
assert(action);
Core::System::GetInstance().SendSignal(Core::System::Signal::Load, action->data().toUInt());
}
void GMainWindow::OnConfigure() {

View File

@@ -4,6 +4,7 @@
#pragma once
#include <array>
#include <memory>
#include <QLabel>
#include <QMainWindow>
@@ -14,6 +15,7 @@
#include "common/announce_multiplayer_room.h"
#include "core/core.h"
#include "core/hle/service/am/am.h"
#include "core/savestate.h"
#include "ui_main.h"
class AboutDialog;
@@ -106,6 +108,7 @@ private:
void InitializeWidgets();
void InitializeDebugWidgets();
void InitializeRecentFileMenuActions();
void InitializeSaveStateMenuActions();
void SetDefaultUIGeometry();
void SyncMenuUISettings();
@@ -149,6 +152,8 @@ private:
*/
void UpdateRecentFiles();
void UpdateSaveStates();
/**
* If the emulation is running,
* asks the user if he really want to close the emulator
@@ -163,8 +168,8 @@ private slots:
void OnStartGame();
void OnPauseGame();
void OnStopGame();
void OnSave();
void OnLoad();
void OnSaveState();
void OnLoadState();
void OnMenuReportCompatibility();
/// Called whenever a user selects a game in the game list widget.
void OnGameListLoadFile(QString game_path);
@@ -276,6 +281,13 @@ private:
bool defer_update_prompt = false;
QAction* actions_recent_files[max_recent_files_item];
std::array<QAction*, Core::SaveStateSlotCount> actions_load_state;
std::array<QAction*, Core::SaveStateSlotCount> actions_save_state;
u32 oldest_slot;
u64 oldest_slot_time;
u32 newest_slot;
u64 newest_slot_time;
QTranslator translator;

View File

@@ -79,17 +79,32 @@
<property name="title">
<string>&amp;Emulation</string>
</property>
<widget class="QMenu" name="menu_Save_State">
<property name="title">
<string>Save State</string>
</property>
<addaction name="action_Save_to_Oldest_Slot"/>
<addaction name="separator"/>
</widget>
<widget class="QMenu" name="menu_Load_State">
<property name="title">
<string>Load State</string>
</property>
<addaction name="action_Load_from_Newest_Slot"/>
<addaction name="separator"/>
</widget>
<addaction name="action_Start"/>
<addaction name="action_Pause"/>
<addaction name="action_Stop"/>
<addaction name="action_Restart"/>
<addaction name="separator"/>
<addaction name="menu_Load_State"/>
<addaction name="menu_Save_State"/>
<addaction name="separator"/>
<addaction name="action_Report_Compatibility"/>
<addaction name="separator"/>
<addaction name="action_Configure"/>
<addaction name="action_Cheats"/>
<addaction name="action_Save"/>
<addaction name="action_Load"/>
</widget>
<widget class="QMenu" name="menu_View">
<property name="title">
@@ -253,6 +268,16 @@
<string>Single Window Mode</string>
</property>
</action>
<action name="action_Save_to_Oldest_Slot">
<property name="text">
<string>Save to Oldest Slot</string>
</property>
</action>
<action name="action_Load_from_Newest_Slot">
<property name="text">
<string>Load from Newest Slot</string>
</property>
</action>
<action name="action_Configure">
<property name="text">
<string>Configure...</string>