@@ -30,9 +30,6 @@ enum
{
SLOT_A = 0,
SLOT_B = 1,
GCI = 0,
SAV = 0x80,
GCS = 0x110,
};

enum class GCMemcardGetSaveDataRetVal
@@ -45,26 +42,10 @@ enum class GCMemcardGetSaveDataRetVal
enum class GCMemcardImportFileRetVal
{
SUCCESS,
FAIL,
NOMEMCARD,
OUTOFDIRENTRIES,
OUTOFBLOCKS,
TITLEPRESENT,
INVALIDFILESIZE,
GCSFAIL,
SAVFAIL,
OPENFAIL,
LENGTHFAIL,
};

enum class GCMemcardExportFileRetVal
{
SUCCESS,
FAIL,
NOMEMCARD,
OPENFAIL,
WRITEFAIL,
UNUSED,
};

enum class GCMemcardRemoveFileRetVal
@@ -400,6 +381,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:
@@ -419,8 +406,6 @@ class GCMemcard

GCMemcard();

GCMemcardImportFileRetVal ImportGciInternal(File::IOFile&& gci, const std::string& inputFile);

const Directory& GetActiveDirectory() const;
const BlockAlloc& GetActiveBat() const;

@@ -496,26 +481,15 @@ class GCMemcard

GCMemcardGetSaveDataRetVal GetSaveData(u8 index, std::vector<GCMBlock>& saveBlocks) const;

// adds the file to the directory and copies its contents
GCMemcardImportFileRetVal ImportFile(const DEntry& direntry, std::vector<GCMBlock>& saveBlocks);
// Adds the given savefile to the memory card, if possible.
GCMemcardImportFileRetVal ImportFile(const Savefile& savefile);

// Fetches the savefile at the given directory index, if any.
std::optional<Savefile> ExportFile(u8 index) const;

// delete a file from the directory
GCMemcardRemoveFileRetVal RemoveFile(u8 index);

// reads a save from another memcard, and imports the data into this memcard
GCMemcardImportFileRetVal CopyFrom(const GCMemcard& source, u8 index);

// reads a .gci/.gcs/.sav file and calls ImportFile
GCMemcardImportFileRetVal ImportGci(const std::string& inputFile);

// writes a .gci file to disk containing index
GCMemcardExportFileRetVal ExportGci(u8 index, const std::string& fileName,
const std::string& directory) const;

// GCI files are untouched, SAV files are byteswapped
// GCS files have the block count set, default is 1 (For export as GCS)
static void Gcs_SavConvert(DEntry& tempDEntry, int saveType, u64 length = BLOCK_SIZE);

// reads the banner image
std::optional<std::vector<u32>> ReadBannerRGBA8(u8 index) const;

@@ -17,6 +17,7 @@

#include "Common/Assert.h"
#include "Common/ChunkFile.h"
#include "Common/CommonPaths.h"
#include "Common/CommonTypes.h"
#include "Common/Config/Config.h"
#include "Common/FileSearch.h"
@@ -32,6 +33,8 @@
#include "Core/ConfigManager.h"
#include "Core/Core.h"
#include "Core/HW/EXI/EXI_DeviceIPL.h"
#include "Core/HW/GCMemcard/GCMemcard.h"
#include "Core/HW/GCMemcard/GCMemcardUtils.h"
#include "Core/HW/Sram.h"
#include "Core/NetPlayProto.h"

@@ -716,7 +719,13 @@ void MigrateFromMemcardFile(const std::string& directory_name, int card_index)
{
for (u8 i = 0; i < Memcard::DIRLEN; i++)
{
memcard->ExportGci(i, "", directory_name);
const auto savefile = memcard->ExportFile(i);
if (!savefile)
continue;

std::string filepath =
directory_name + DIR_SEP + Memcard::GenerateFilename(savefile->dir_entry) + ".gci";
Memcard::WriteSavefile(filepath, *savefile, Memcard::SavefileFormat::GCI);
}
}
}
@@ -1,9 +1,27 @@
#include "Core/HW/GCMemcard/GCMemcardUtils.h"

#include <array>
#include <cassert>
#include <string>
#include <vector>

#include "Common/CommonTypes.h"
#include "Common/IOFile.h"
#include "Common/NandPaths.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
@@ -34,7 +52,7 @@ bool HasSameIdentity(const DEntry& lhs, const DEntry& rhs)

// With all that in mind, even if it mismatches the comparison behavior of the BIOS, we treat
// m_filename as a nullterminated string for determining if two files identify as the same, as not
// doing so would cause more harm and confusion that good in practice.
// doing so would cause more harm and confusion than good in practice.

if (lhs.m_gamecode != rhs.m_gamecode)
return false;
@@ -53,4 +71,289 @@ bool HasSameIdentity(const DEntry& lhs, const DEntry& rhs)

return true;
}

bool HasDuplicateIdentity(const std::vector<Savefile>& savefiles)
{
for (size_t i = 0; i < savefiles.size(); ++i)
{
for (size_t j = i + 1; j < savefiles.size(); ++j)
{
if (HasSameIdentity(savefiles[i].dir_entry, savefiles[j].dir_entry))
return true;
}
}
return false;
}

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;
}
}

std::string GenerateFilename(const DEntry& entry)
{
std::string maker(reinterpret_cast<const char*>(entry.m_makercode.data()),
entry.m_makercode.size());
std::string gamecode(reinterpret_cast<const char*>(entry.m_gamecode.data()),
entry.m_gamecode.size());

// prevent going out of bounds when all bytes of m_filename are non-null
size_t length = 0;
for (size_t i = 0; i < entry.m_filename.size(); ++i)
{
if (entry.m_filename[i] == 0)
break;
++length;
}
std::string filename(reinterpret_cast<const char*>(entry.m_filename.data()), length);

return Common::EscapeFileName(maker + '-' + gamecode + '-' + filename);
}

std::string GetDefaultExtension(SavefileFormat format)
{
switch (format)
{
case SavefileFormat::GCI:
return ".gci";
case SavefileFormat::GCS:
return ".gcs";
case SavefileFormat::SAV:
return ".sav";
default:
assert(0);
return ".gci";
}
}

std::vector<Savefile> GetSavefiles(const GCMemcard& card, const std::vector<u8>& file_indices)
{
std::vector<Savefile> files;
files.reserve(file_indices.size());
for (const u8 index : file_indices)
{
std::optional<Savefile> file = card.ExportFile(index);
if (!file)
return {};
files.emplace_back(std::move(*file));
}
return files;
}

size_t GetBlockCount(const std::vector<Savefile>& savefiles)
{
size_t block_count = 0;
for (const Savefile& savefile : savefiles)
block_count += savefile.blocks.size();
return block_count;
}
} // namespace Memcard
@@ -4,9 +4,48 @@

#pragma once

#include <string>
#include <variant>

#include "Core/HW/GCMemcard/GCMemcard.h"

namespace Memcard
{
struct DEntry;

bool HasSameIdentity(const DEntry& lhs, const DEntry& rhs);

// Check if any two given savefiles have the same identity.
bool HasDuplicateIdentity(const std::vector<Savefile>& savefiles);

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);

// Generates a filename (without extension) for the given directory entry.
std::string GenerateFilename(const DEntry& entry);

// Returns the expected extension for a filename in the given format. Includes the leading dot.
std::string GetDefaultExtension(SavefileFormat format);

// Reads multiple savefiles from a card. Returns empty vector if even a single file can't be read.
std::vector<Savefile> GetSavefiles(const GCMemcard& card, const std::vector<u8>& file_indices);

// Gets the total amount of blocks the given saves use.
size_t GetBlockCount(const std::vector<Savefile>& savefiles);
} // namespace Memcard

Large diffs are not rendered by default.

@@ -5,6 +5,7 @@
#pragma once

#include <array>
#include <map>
#include <memory>
#include <utility>
#include <vector>
@@ -17,17 +18,23 @@ namespace Memcard
{
class GCMemcard;
class GCMemcardErrorCode;
struct Savefile;
enum class ReadSavefileErrorCode;
enum class SavefileFormat;
} // namespace Memcard

class QAction;
class QDialogButtonBox;
class QGroupBox;
class QLabel;
class QLineEdit;
class QMenu;
class QPixmap;
class QPushButton;
class QString;
class QTableWidget;
class QTimer;
class QToolButton;

class GCMemcardManager : public QDialog
{
@@ -37,6 +44,7 @@ class GCMemcardManager : public QDialog
~GCMemcardManager();

static QString GetErrorMessagesForErrorCode(const Memcard::GCMemcardErrorCode& code);
static QString GetErrorMessageForErrorCode(Memcard::ReadSavefileErrorCode code);

private:
struct IconAnimationData;
@@ -51,11 +59,14 @@ class GCMemcardManager : public QDialog
void SetSlotFileInteractive(int slot);
void SetActiveSlot(int slot);

std::vector<u8> GetSelectedFileIndices();

void ImportFiles(int slot, const std::vector<Memcard::Savefile>& savefiles);

void CopyFiles();
void ImportFile();
void DeleteFiles();
void ExportFiles(bool prompt);
void ExportAllFiles();
void ExportFiles(Memcard::SavefileFormat format);
void FixChecksums();
void CreateNewCard(int slot);
void DrawIcons();
@@ -67,15 +78,18 @@ class GCMemcardManager : public QDialog
// Actions
QPushButton* m_select_button;
QPushButton* m_copy_button;
QPushButton* m_export_button;
QPushButton* m_export_all_button;
QToolButton* m_export_button;
QMenu* m_export_menu;
QAction* m_export_gci_action;
QAction* m_export_gcs_action;
QAction* m_export_sav_action;
QPushButton* m_import_button;
QPushButton* m_delete_button;
QPushButton* m_fix_checksums_button;

// Slots
static constexpr int SLOT_COUNT = 2;
std::array<std::vector<IconAnimationData>, SLOT_COUNT> m_slot_active_icons;
std::array<std::map<u8, IconAnimationData>, SLOT_COUNT> m_slot_active_icons;
std::array<std::unique_ptr<Memcard::GCMemcard>, SLOT_COUNT> m_slot_memcard;
std::array<QGroupBox*, SLOT_COUNT> m_slot_group;
std::array<QLineEdit*, SLOT_COUNT> m_slot_file_edit;