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:
parent
7d880f94db
commit
a487016cb4
11 changed files with 384 additions and 77 deletions
|
@ -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() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue