| | |
| | |
| | |
| |
|
| | #include <chrono> |
| | #include <sstream> |
| | #include <cryptopp/hex.h> |
| | #include <fmt/format.h> |
| | #include "common/archives.h" |
| | #include "common/file_util.h" |
| | #include "common/logging/log.h" |
| | #include "common/scm_rev.h" |
| | #include "common/swap.h" |
| | #include "common/zstd_compression.h" |
| | #include "core/core.h" |
| | #include "core/movie.h" |
| | #include "core/savestate.h" |
| | #include "core/savestate_data.h" |
| | #include "network/network.h" |
| |
|
| | namespace Core { |
| |
|
| | #pragma pack(push, 1) |
| | struct CSTHeader { |
| | std::array<u8, 4> filetype; |
| | u64_le program_id; |
| | std::array<u8, 20> revision; |
| | u64_le time; |
| | std::array<u8, 20> build_name; |
| | u32_le zero = 0; |
| |
|
| | std::array<u8, 192> reserved{}; |
| | }; |
| | 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}}; |
| |
|
| | static std::string GetSaveStatePath(u64 program_id, u64 movie_id, u32 slot) { |
| | if (movie_id) { |
| | return fmt::format("{}{:016X}.movie{:016X}.{:02d}.cst", |
| | FileUtil::GetUserPath(FileUtil::UserPath::StatesDir), program_id, |
| | movie_id, slot); |
| | } else { |
| | return fmt::format("{}{:016X}.{:02d}.cst", |
| | FileUtil::GetUserPath(FileUtil::UserPath::StatesDir), program_id, slot); |
| | } |
| | } |
| |
|
| | static bool ValidateSaveState(const CSTHeader& header, SaveStateInfo& info, u64 program_id, |
| | u64 movie_id) { |
| | const auto path = GetSaveStatePath(program_id, movie_id, info.slot); |
| | if (header.filetype != header_magic_bytes) { |
| | LOG_WARNING(Core, "Invalid save state file {}", path); |
| | return false; |
| | } |
| | info.time = header.time; |
| |
|
| | if (header.program_id != program_id) { |
| | LOG_WARNING(Core, "Save state file isn't for the current game {}", path); |
| | return false; |
| | } |
| | const std::string revision = fmt::format("{:02x}", fmt::join(header.revision, "")); |
| | const std::string build_name = |
| | header.zero == 0 ? reinterpret_cast<const char*>(header.build_name.data()) : ""; |
| |
|
| | if (revision == Common::g_scm_rev) { |
| | info.status = SaveStateInfo::ValidationStatus::OK; |
| | } else { |
| | if (!build_name.empty()) { |
| | info.build_name = build_name; |
| | } else if (hash_to_version.find(revision) != hash_to_version.end()) { |
| | info.build_name = hash_to_version.at(revision); |
| | } |
| | if (info.build_name.empty()) { |
| | LOG_WARNING(Core, "Save state file {} created from a different revision {}", path, |
| | revision); |
| | } else { |
| | LOG_WARNING(Core, |
| | "Save state file {} created from a different build {} with revision {}", |
| | path, info.build_name, revision); |
| | } |
| |
|
| | info.status = SaveStateInfo::ValidationStatus::RevisionDismatch; |
| | } |
| | return true; |
| | } |
| |
|
| | std::vector<SaveStateInfo> ListSaveStates(u64 program_id, u64 movie_id) { |
| | std::vector<SaveStateInfo> result; |
| | result.reserve(SaveStateSlotCount); |
| | for (u32 slot = 1; slot <= SaveStateSlotCount; ++slot) { |
| | const auto path = GetSaveStatePath(program_id, movie_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 (!ValidateSaveState(header, info, program_id, movie_id)) { |
| | continue; |
| | } |
| |
|
| | result.emplace_back(std::move(info)); |
| | } |
| | return result; |
| | } |
| |
|
| | void System::SaveState(u32 slot) const { |
| | std::ostringstream sstream{std::ios_base::binary}; |
| | |
| | oarchive oa{sstream}; |
| | oa&* this; |
| |
|
| | const std::string& str{sstream.str()}; |
| | const auto data = std::span<const u8>{reinterpret_cast<const u8*>(str.data()), str.size()}; |
| | auto buffer = Common::Compression::CompressDataZSTDDefault(data); |
| |
|
| | const u64 movie_id = movie.GetCurrentMovieID(); |
| | const auto path = GetSaveStatePath(title_id, movie_id, slot); |
| | if (!FileUtil::CreateFullPath(path)) { |
| | throw std::runtime_error("Could not create path " + path); |
| | } |
| |
|
| | FileUtil::IOFile file(path, "wb"); |
| | if (!file) { |
| | throw std::runtime_error("Could not open file " + path); |
| | } |
| |
|
| | CSTHeader header{}; |
| | header.filetype = header_magic_bytes; |
| | header.program_id = title_id; |
| | std::string rev_bytes; |
| | CryptoPP::StringSource ss(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(); |
| | const std::string build_fullname = Common::g_build_fullname; |
| | std::memset(header.build_name.data(), 0, sizeof(header.build_name)); |
| | std::memcpy(header.build_name.data(), build_fullname.c_str(), |
| | std::min(build_fullname.length(), sizeof(header.build_name) - 1)); |
| |
|
| | if (file.WriteBytes(&header, sizeof(header)) != sizeof(header) || |
| | file.WriteBytes(buffer.data(), buffer.size()) != buffer.size()) { |
| | throw std::runtime_error("Could not write to file " + path); |
| | } |
| | } |
| |
|
| | void System::LoadState(u32 slot) { |
| | if (Network::GetRoomMember().lock()->IsConnected()) { |
| | throw std::runtime_error("Unable to load while connected to multiplayer"); |
| | } |
| |
|
| | const u64 movie_id = movie.GetCurrentMovieID(); |
| | const auto path = GetSaveStatePath(title_id, movie_id, slot); |
| |
|
| | std::vector<u8> decompressed; |
| | { |
| | std::vector<u8> buffer(FileUtil::GetSize(path) - sizeof(CSTHeader)); |
| |
|
| | FileUtil::IOFile file(path, "rb"); |
| |
|
| | |
| | CSTHeader header; |
| | if (file.ReadBytes(&header, sizeof(header)) != sizeof(header)) { |
| | throw std::runtime_error("Could not read from file at " + path); |
| | } |
| |
|
| | |
| | SaveStateInfo info; |
| | info.slot = slot; |
| | if (!ValidateSaveState(header, info, title_id, movie_id)) { |
| | throw std::runtime_error("Invalid savestate"); |
| | } |
| |
|
| | if (file.ReadBytes(buffer.data(), buffer.size()) != buffer.size()) { |
| | throw std::runtime_error("Could not read from file at " + path); |
| | } |
| | decompressed = Common::Compression::DecompressDataZSTD(buffer); |
| | } |
| | std::istringstream sstream{ |
| | std::string{reinterpret_cast<char*>(decompressed.data()), decompressed.size()}, |
| | std::ios_base::binary}; |
| | decompressed.clear(); |
| |
|
| | |
| | iarchive ia{sstream}; |
| | ia&* this; |
| | } |
| |
|
| | } |
| |
|