Skip to content

Commit

Permalink
GCMemcard: Implement utility functions to read saves from and write s…
Browse files Browse the repository at this point in the history
…aves to files, without involving a memory card.
  • Loading branch information
AdmiralCurtiss committed Jan 28, 2021
1 parent 2f661fe commit 9b14cc8
Show file tree
Hide file tree
Showing 3 changed files with 262 additions and 2 deletions.
6 changes: 6 additions & 0 deletions Source/Core/Core/HW/GCMemcard/GCMemcard.h
Expand Up @@ -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:
Expand Down
230 changes: 230 additions & 0 deletions Source/Core/Core/HW/GCMemcard/GCMemcardUtils.cpp
@@ -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
Expand Down Expand Up @@ -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
28 changes: 26 additions & 2 deletions Source/Core/Core/HW/GCMemcard/GCMemcardUtils.h
Expand Up @@ -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

0 comments on commit 9b14cc8

Please sign in to comment.