GCMemcard: Implement utility functions to read saves from and write saves to files, without involving a memory card.

This commit is contained in:
Admiral H. Curtiss 2020-07-11 02:04:40 +02:00
parent 2f661fec23
commit 9b14cc8ea2
3 changed files with 262 additions and 2 deletions

View File

@ -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:

View File

@ -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

View File

@ -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