GCMemcard: Implement utility functions to read saves from and write saves to files, without involving a memory card.
This commit is contained in:
parent
2f661fec23
commit
9b14cc8ea2
|
@ -400,6 +400,12 @@ static_assert(sizeof(BlockAlloc) == BLOCK_SIZE);
|
|||
static_assert(std::is_trivially_copyable_v<BlockAlloc>);
|
||||
#pragma pack(pop)
|
||||
|
||||
struct Savefile
|
||||
{
|
||||
DEntry dir_entry;
|
||||
std::vector<GCMBlock> blocks;
|
||||
};
|
||||
|
||||
class GCMemcard
|
||||
{
|
||||
private:
|
||||
|
|
|
@ -1,9 +1,25 @@
|
|||
#include "Core/HW/GCMemcard/GCMemcardUtils.h"
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/IOFile.h"
|
||||
|
||||
#include "Core/HW/GCMemcard/GCMemcard.h"
|
||||
|
||||
namespace Memcard
|
||||
{
|
||||
constexpr u32 GCI_HEADER_SIZE = DENTRY_SIZE;
|
||||
constexpr std::array<u8, 12> SAV_MAGIC = {0x44, 0x41, 0x54, 0x45, 0x4C, 0x47,
|
||||
0x43, 0x5F, 0x53, 0x41, 0x56, 0x45}; // "DATELGC_SAVE"
|
||||
constexpr u32 SAV_HEADER_SIZE = 0xC0;
|
||||
constexpr u32 SAV_DENTRY_OFFSET = 0x80;
|
||||
constexpr std::array<u8, 6> GCS_MAGIC = {0x47, 0x43, 0x53, 0x41, 0x56, 0x45}; // "GCSAVE"
|
||||
constexpr u32 GCS_HEADER_SIZE = 0x150;
|
||||
constexpr u32 GCS_DENTRY_OFFSET = 0x110;
|
||||
|
||||
bool HasSameIdentity(const DEntry& lhs, const DEntry& rhs)
|
||||
{
|
||||
// The Gamecube BIOS identifies two files as being 'the same' (that is, disallows copying from one
|
||||
|
@ -53,4 +69,218 @@ bool HasSameIdentity(const DEntry& lhs, const DEntry& rhs)
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ByteswapDEntrySavHeader(std::array<u8, DENTRY_SIZE>& entry)
|
||||
{
|
||||
// several bytes in SAV are swapped compared to the internal memory card format
|
||||
for (size_t p : {0x06, 0x2C, 0x2E, 0x30, 0x32, 0x34, 0x36, 0x38, 0x3A, 0x3C, 0x3E})
|
||||
std::swap(entry[p], entry[p + 1]);
|
||||
}
|
||||
|
||||
static DEntry ExtractDEntryFromSavHeader(const std::array<u8, SAV_HEADER_SIZE>& sav_header)
|
||||
{
|
||||
std::array<u8, DENTRY_SIZE> entry;
|
||||
std::memcpy(entry.data(), &sav_header[SAV_DENTRY_OFFSET], DENTRY_SIZE);
|
||||
ByteswapDEntrySavHeader(entry);
|
||||
|
||||
DEntry dir_entry;
|
||||
std::memcpy(&dir_entry, entry.data(), DENTRY_SIZE);
|
||||
return dir_entry;
|
||||
}
|
||||
|
||||
static void InjectDEntryToSavHeader(std::array<u8, SAV_HEADER_SIZE>& sav_header,
|
||||
const DEntry& dir_entry)
|
||||
{
|
||||
std::array<u8, DENTRY_SIZE> entry;
|
||||
std::memcpy(entry.data(), &dir_entry, DENTRY_SIZE);
|
||||
ByteswapDEntrySavHeader(entry);
|
||||
std::memcpy(&sav_header[SAV_DENTRY_OFFSET], entry.data(), DENTRY_SIZE);
|
||||
}
|
||||
|
||||
static bool ReadBlocksFromIOFile(File::IOFile& file, std::vector<GCMBlock>& blocks,
|
||||
size_t block_count)
|
||||
{
|
||||
blocks.reserve(block_count);
|
||||
for (size_t i = 0; i < block_count; ++i)
|
||||
{
|
||||
GCMBlock& block = blocks.emplace_back();
|
||||
if (!file.ReadBytes(block.m_block.data(), block.m_block.size()))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::variant<ReadSavefileErrorCode, Savefile> ReadSavefileInternalGCI(File::IOFile& file,
|
||||
u64 filesize)
|
||||
{
|
||||
Savefile savefile;
|
||||
if (!file.ReadBytes(&savefile.dir_entry, DENTRY_SIZE))
|
||||
return ReadSavefileErrorCode::IOError;
|
||||
|
||||
const size_t block_count = savefile.dir_entry.m_block_count;
|
||||
const u64 expected_size = DENTRY_SIZE + block_count * BLOCK_SIZE;
|
||||
if (expected_size != filesize)
|
||||
return ReadSavefileErrorCode::DataCorrupted;
|
||||
|
||||
if (!ReadBlocksFromIOFile(file, savefile.blocks, block_count))
|
||||
return ReadSavefileErrorCode::IOError;
|
||||
|
||||
return savefile;
|
||||
}
|
||||
|
||||
static std::variant<ReadSavefileErrorCode, Savefile> ReadSavefileInternalGCS(File::IOFile& file,
|
||||
u64 filesize)
|
||||
{
|
||||
std::array<u8, GCS_HEADER_SIZE> gcs_header;
|
||||
if (!file.ReadBytes(gcs_header.data(), gcs_header.size()))
|
||||
return ReadSavefileErrorCode::IOError;
|
||||
|
||||
if (std::memcmp(gcs_header.data(), GCS_MAGIC.data(), GCS_MAGIC.size()) != 0)
|
||||
return ReadSavefileErrorCode::DataCorrupted;
|
||||
|
||||
Savefile savefile;
|
||||
std::memcpy(&savefile.dir_entry, &gcs_header[GCS_DENTRY_OFFSET], DENTRY_SIZE);
|
||||
|
||||
// field containing the Block count as displayed within
|
||||
// the GameSaves software is not stored in the GCS file.
|
||||
// It is stored only within the corresponding GSV file.
|
||||
// If the GCS file is added without using the GameSaves software,
|
||||
// the value stored is always "1"
|
||||
|
||||
// to get the actual block count calculate backwards from the filesize
|
||||
const u64 total_block_size = filesize - GCS_HEADER_SIZE;
|
||||
if ((total_block_size % BLOCK_SIZE) != 0)
|
||||
return ReadSavefileErrorCode::DataCorrupted;
|
||||
|
||||
const size_t block_count = total_block_size / BLOCK_SIZE;
|
||||
savefile.dir_entry.m_block_count = static_cast<u16>(block_count);
|
||||
|
||||
if (!ReadBlocksFromIOFile(file, savefile.blocks, block_count))
|
||||
return ReadSavefileErrorCode::IOError;
|
||||
|
||||
return savefile;
|
||||
}
|
||||
|
||||
static std::variant<ReadSavefileErrorCode, Savefile> ReadSavefileInternalSAV(File::IOFile& file,
|
||||
u64 filesize)
|
||||
{
|
||||
std::array<u8, SAV_HEADER_SIZE> sav_header;
|
||||
if (!file.ReadBytes(sav_header.data(), sav_header.size()))
|
||||
return ReadSavefileErrorCode::IOError;
|
||||
|
||||
if (std::memcmp(sav_header.data(), SAV_MAGIC.data(), SAV_MAGIC.size()) != 0)
|
||||
return ReadSavefileErrorCode::DataCorrupted;
|
||||
|
||||
Savefile savefile;
|
||||
savefile.dir_entry = ExtractDEntryFromSavHeader(sav_header);
|
||||
|
||||
const size_t block_count = savefile.dir_entry.m_block_count;
|
||||
const u64 expected_size = SAV_HEADER_SIZE + block_count * BLOCK_SIZE;
|
||||
if (expected_size != filesize)
|
||||
return ReadSavefileErrorCode::DataCorrupted;
|
||||
|
||||
if (!ReadBlocksFromIOFile(file, savefile.blocks, block_count))
|
||||
return ReadSavefileErrorCode::IOError;
|
||||
|
||||
return savefile;
|
||||
}
|
||||
|
||||
std::variant<ReadSavefileErrorCode, Savefile> ReadSavefile(const std::string& filename)
|
||||
{
|
||||
File::IOFile file(filename, "rb");
|
||||
if (!file)
|
||||
return ReadSavefileErrorCode::OpenFileFail;
|
||||
|
||||
// Since GCI, GCS and SAV all have different header lengths but the block size is always the same,
|
||||
// we can detect the type from the filesize.
|
||||
const u64 filesize = file.GetSize();
|
||||
const u64 header_size = filesize % BLOCK_SIZE;
|
||||
|
||||
switch (header_size)
|
||||
{
|
||||
case GCI_HEADER_SIZE:
|
||||
return ReadSavefileInternalGCI(file, filesize);
|
||||
case GCS_HEADER_SIZE:
|
||||
return ReadSavefileInternalGCS(file, filesize);
|
||||
case SAV_HEADER_SIZE:
|
||||
return ReadSavefileInternalSAV(file, filesize);
|
||||
default:
|
||||
return ReadSavefileErrorCode::DataCorrupted;
|
||||
}
|
||||
}
|
||||
|
||||
static bool WriteSavefileInternalGCI(File::IOFile& file, const Savefile& savefile)
|
||||
{
|
||||
if (!file.WriteBytes(&savefile.dir_entry, DENTRY_SIZE))
|
||||
return false;
|
||||
|
||||
for (const GCMBlock& block : savefile.blocks)
|
||||
{
|
||||
if (!file.WriteBytes(block.m_block.data(), block.m_block.size()))
|
||||
return false;
|
||||
}
|
||||
|
||||
return file.IsGood();
|
||||
}
|
||||
|
||||
static bool WriteSavefileInternalGCS(File::IOFile& file, const Savefile& savefile)
|
||||
{
|
||||
std::array<u8, GCS_HEADER_SIZE> header;
|
||||
std::memset(header.data(), 0, header.size());
|
||||
std::memcpy(header.data(), GCS_MAGIC.data(), GCS_MAGIC.size());
|
||||
|
||||
DEntry gcs_entry = savefile.dir_entry;
|
||||
gcs_entry.m_block_count = 1; // always stored as 1 in GCS files
|
||||
std::memcpy(&header[GCS_DENTRY_OFFSET], &gcs_entry, DENTRY_SIZE);
|
||||
|
||||
if (!file.WriteBytes(header.data(), header.size()))
|
||||
return false;
|
||||
|
||||
for (const GCMBlock& block : savefile.blocks)
|
||||
{
|
||||
if (!file.WriteBytes(block.m_block.data(), block.m_block.size()))
|
||||
return false;
|
||||
}
|
||||
|
||||
return file.IsGood();
|
||||
}
|
||||
|
||||
static bool WriteSavefileInternalSAV(File::IOFile& file, const Savefile& savefile)
|
||||
{
|
||||
std::array<u8, SAV_HEADER_SIZE> header;
|
||||
std::memset(header.data(), 0, header.size());
|
||||
std::memcpy(header.data(), SAV_MAGIC.data(), SAV_MAGIC.size());
|
||||
|
||||
InjectDEntryToSavHeader(header, savefile.dir_entry);
|
||||
|
||||
if (!file.WriteBytes(header.data(), header.size()))
|
||||
return false;
|
||||
|
||||
for (const GCMBlock& block : savefile.blocks)
|
||||
{
|
||||
if (!file.WriteBytes(block.m_block.data(), block.m_block.size()))
|
||||
return false;
|
||||
}
|
||||
|
||||
return file.IsGood();
|
||||
}
|
||||
|
||||
bool WriteSavefile(const std::string& filename, const Savefile& savefile, SavefileFormat format)
|
||||
{
|
||||
File::IOFile file(filename, "wb");
|
||||
if (!file)
|
||||
return false;
|
||||
|
||||
switch (format)
|
||||
{
|
||||
case SavefileFormat::GCI:
|
||||
return WriteSavefileInternalGCI(file, savefile);
|
||||
case SavefileFormat::GCS:
|
||||
return WriteSavefileInternalGCS(file, savefile);
|
||||
case SavefileFormat::SAV:
|
||||
return WriteSavefileInternalSAV(file, savefile);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} // namespace Memcard
|
||||
|
|
|
@ -4,9 +4,33 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
#include "Core/HW/GCMemcard/GCMemcard.h"
|
||||
|
||||
namespace Memcard
|
||||
{
|
||||
struct DEntry;
|
||||
|
||||
bool HasSameIdentity(const DEntry& lhs, const DEntry& rhs);
|
||||
|
||||
enum class ReadSavefileErrorCode
|
||||
{
|
||||
OpenFileFail,
|
||||
IOError,
|
||||
DataCorrupted,
|
||||
};
|
||||
|
||||
// Reads a Gamecube memory card savefile from a file.
|
||||
// Supported formats are GCI, GCS (Gameshark), and SAV (MaxDrive).
|
||||
std::variant<ReadSavefileErrorCode, Savefile> ReadSavefile(const std::string& filename);
|
||||
|
||||
enum class SavefileFormat
|
||||
{
|
||||
GCI,
|
||||
GCS,
|
||||
SAV,
|
||||
};
|
||||
|
||||
// Writes a Gamecube memory card savefile to a file.
|
||||
bool WriteSavefile(const std::string& filename, const Savefile& savefile, SavefileFormat format);
|
||||
} // namespace Memcard
|
||||
|
|
Loading…
Reference in New Issue