citra/src/core/savestate.cpp
zhupengfei a487016cb4
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`.
2020-02-18 13:19:52 +08:00

174 lines
5.6 KiB
C++

// Copyright 2020 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <chrono>
#include <cryptopp/hex.h>
#include "common/archives.h"
#include "common/logging/log.h"
#include "common/scm_rev.h"
#include "common/zstd_compression.h"
#include "core/core.h"
#include "core/savestate.h"
#include "video_core/video_core.h"
namespace Core {
#pragma pack(push, 1)
struct CSTHeader {
std::array<u8, 4> filetype; /// Unique Identifier to check the file type (always "CST"0x1B)
u64_le program_id; /// ID of the ROM being executed. Also called title_id
std::array<u8, 20> revision; /// Git hash of the revision this savestate was created with
u64_le time; /// The time when this save state was created
std::array<u8, 216> reserved; /// Make heading 256 bytes so it has consistent size
};
static_assert(sizeof(CSTHeader) == 256, "CSTHeader should be 256 bytes");
#pragma pack(pop)
constexpr std::array<u8, 4> header_magic_bytes{{'C', 'S', 'T', 0x1B}};
std::string GetSaveStatePath(u64 program_id, u32 slot) {
return fmt::format("{}{:016X}.{:02d}.cst", FileUtil::GetUserPath(FileUtil::UserPath::StatesDir),
program_id, slot);
}
std::vector<SaveStateInfo> ListSaveStates(u64 program_id) {
std::vector<SaveStateInfo> result;
for (u32 slot = 1; slot <= SaveStateSlotCount; ++slot) {
const auto path = GetSaveStatePath(program_id, slot);
if (!FileUtil::Exists(path)) {
continue;
}
SaveStateInfo info;
info.slot = slot;
FileUtil::IOFile file(path, "rb");
if (!file) {
LOG_ERROR(Core, "Could not open file {}", path);
continue;
}
CSTHeader header;
if (file.GetSize() < sizeof(header)) {
LOG_ERROR(Core, "File too small {}", path);
continue;
}
if (file.ReadBytes(&header, sizeof(header)) != sizeof(header)) {
LOG_ERROR(Core, "Could not read from file {}", path);
continue;
}
if (header.filetype != header_magic_bytes) {
LOG_WARNING(Core, "Invalid save state file {}", path);
continue;
}
info.time = header.time;
if (header.program_id != program_id) {
LOG_WARNING(Core, "Save state file isn't for the current game {}", path);
continue;
}
std::string revision = fmt::format("{:02x}", fmt::join(header.revision, ""));
if (revision == Common::g_scm_rev) {
info.status = SaveStateInfo::ValidationStatus::OK;
} else {
LOG_WARNING(Core, "Save state file created from a different revision {}", path);
info.status = SaveStateInfo::ValidationStatus::RevisionDismatch;
}
result.emplace_back(std::move(info));
}
return result;
}
void System::SaveState(u32 slot) const {
std::ostringstream sstream{std::ios_base::binary};
try {
{
oarchive oa{sstream};
oa&* this;
}
VideoCore::Save(sstream);
} catch (const std::exception& e) {
LOG_ERROR(Core, "Error saving: {}", e.what());
}
const std::string& str{sstream.str()};
auto buffer = Common::Compression::CompressDataZSTDDefault(
reinterpret_cast<const u8*>(str.data()), str.size());
const auto path = GetSaveStatePath(title_id, slot);
if (!FileUtil::CreateFullPath(path)) {
LOG_ERROR(Core, "Could not create path {}", path);
return;
}
FileUtil::IOFile file(path, "wb");
if (!file) {
LOG_ERROR(Core, "Could not open file {}", path);
return;
}
CSTHeader header{};
header.filetype = header_magic_bytes;
header.program_id = title_id;
std::string rev_bytes;
CryptoPP::StringSource(Common::g_scm_rev, true,
new CryptoPP::HexDecoder(new CryptoPP::StringSink(rev_bytes)));
std::memcpy(header.revision.data(), rev_bytes.data(), sizeof(header.revision));
header.time = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
if (file.WriteBytes(&header, sizeof(header)) != sizeof(header)) {
LOG_ERROR(Core, "Could not write to file {}", path);
return;
}
if (file.WriteBytes(buffer.data(), buffer.size()) != buffer.size()) {
LOG_ERROR(Core, "Could not write to file {}", path);
return;
}
}
void System::LoadState(u32 slot) {
const auto path = GetSaveStatePath(title_id, slot);
if (!FileUtil::Exists(path)) {
LOG_ERROR(Core, "File not exist {}", path);
return;
}
std::vector<u8> decompressed;
{
std::vector<u8> buffer(FileUtil::GetSize(path) - sizeof(CSTHeader));
FileUtil::IOFile file(path, "rb");
if (!file) {
LOG_ERROR(Core, "Could not open file {}", path);
return;
}
file.Seek(sizeof(CSTHeader), SEEK_SET); // Skip header
if (file.ReadBytes(buffer.data(), buffer.size()) != buffer.size()) {
LOG_ERROR(Core, "Could not read from file {}", path);
return;
}
decompressed = Common::Compression::DecompressDataZSTD(buffer);
}
std::istringstream sstream{
std::string{reinterpret_cast<char*>(decompressed.data()), decompressed.size()},
std::ios_base::binary};
decompressed.clear();
try {
{
iarchive ia{sstream};
ia&* this;
}
VideoCore::Load(sstream);
} catch (const std::exception& e) {
LOG_ERROR(Core, "Error loading: {}", e.what());
}
}
} // namespace Core