mirror of
https://github.com/izzy2lost/dolphin.git
synced 2026-06-19 01:16:48 -07:00
Merge pull request #8996 from AdmiralCurtiss/memcard-save-import-export-refactor
Various improvements for the Memory Card Manager.
This commit is contained in:
@@ -26,3 +26,12 @@ auto VariantCast(const std::variant<From...>& v)
|
||||
{
|
||||
return detail::VariantCastProxy<From...>{v};
|
||||
}
|
||||
|
||||
template <class... Ts>
|
||||
struct overloaded : Ts...
|
||||
{
|
||||
using Ts::operator()...;
|
||||
};
|
||||
|
||||
template <class... Ts>
|
||||
overloaded(Ts...) -> overloaded<Ts...>;
|
||||
|
||||
@@ -432,9 +432,6 @@ bool GCMemcard::GCI_FileName(u8 index, std::string& filename) const
|
||||
return true;
|
||||
}
|
||||
|
||||
// DEntry functions, all take u8 index < DIRLEN (127)
|
||||
// Functions that have ascii output take a char *buffer
|
||||
|
||||
std::string GCMemcard::DEntry_GameCode(u8 index) const
|
||||
{
|
||||
if (!m_valid || index >= DIRLEN)
|
||||
@@ -836,14 +833,14 @@ GCMemcardGetSaveDataRetVal GCMemcard::GetSaveData(u8 index, std::vector<GCMBlock
|
||||
}
|
||||
return GCMemcardGetSaveDataRetVal::SUCCESS;
|
||||
}
|
||||
// End DEntry functions
|
||||
|
||||
GCMemcardImportFileRetVal GCMemcard::ImportFile(const DEntry& direntry,
|
||||
std::vector<GCMBlock>& saveBlocks)
|
||||
GCMemcardImportFileRetVal GCMemcard::ImportFile(const Savefile& savefile)
|
||||
{
|
||||
if (!m_valid)
|
||||
return GCMemcardImportFileRetVal::NOMEMCARD;
|
||||
|
||||
const DEntry& direntry = savefile.dir_entry;
|
||||
|
||||
if (GetNumFiles() >= DIRLEN)
|
||||
{
|
||||
return GCMemcardImportFileRetVal::OUTOFDIRENTRIES;
|
||||
@@ -880,8 +877,9 @@ GCMemcardImportFileRetVal GCMemcard::ImportFile(const DEntry& direntry,
|
||||
|
||||
int fileBlocks = direntry.m_block_count;
|
||||
|
||||
FZEROGX_MakeSaveGameValid(m_header_block, direntry, saveBlocks);
|
||||
PSO_MakeSaveGameValid(m_header_block, direntry, saveBlocks);
|
||||
std::vector<GCMBlock> blocks = savefile.blocks;
|
||||
FZEROGX_MakeSaveGameValid(m_header_block, direntry, blocks);
|
||||
PSO_MakeSaveGameValid(m_header_block, direntry, blocks);
|
||||
|
||||
BlockAlloc UpdatedBat = GetActiveBat();
|
||||
u16 nextBlock;
|
||||
@@ -890,7 +888,7 @@ GCMemcardImportFileRetVal GCMemcard::ImportFile(const DEntry& direntry,
|
||||
{
|
||||
if (firstBlock == 0xFFFF)
|
||||
PanicAlertFmt("Fatal Error");
|
||||
m_data_blocks[firstBlock - MC_FST_BLOCKS] = saveBlocks[i];
|
||||
m_data_blocks[firstBlock - MC_FST_BLOCKS] = blocks[i];
|
||||
if (i == fileBlocks - 1)
|
||||
nextBlock = 0xFFFF;
|
||||
else
|
||||
@@ -909,6 +907,22 @@ GCMemcardImportFileRetVal GCMemcard::ImportFile(const DEntry& direntry,
|
||||
return GCMemcardImportFileRetVal::SUCCESS;
|
||||
}
|
||||
|
||||
std::optional<Savefile> GCMemcard::ExportFile(u8 index) const
|
||||
{
|
||||
if (!m_valid || index >= DIRLEN)
|
||||
return std::nullopt;
|
||||
|
||||
Savefile savefile;
|
||||
savefile.dir_entry = GetActiveDirectory().m_dir_entries[index];
|
||||
if (savefile.dir_entry.m_gamecode == DEntry::UNINITIALIZED_GAMECODE)
|
||||
return std::nullopt;
|
||||
|
||||
if (GetSaveData(index, savefile.blocks) != GCMemcardGetSaveDataRetVal::SUCCESS)
|
||||
return std::nullopt;
|
||||
|
||||
return savefile;
|
||||
}
|
||||
|
||||
GCMemcardRemoveFileRetVal GCMemcard::RemoveFile(u8 index) // index in the directory array
|
||||
{
|
||||
if (!m_valid)
|
||||
@@ -940,250 +954,6 @@ GCMemcardRemoveFileRetVal GCMemcard::RemoveFile(u8 index) // index in the direc
|
||||
return GCMemcardRemoveFileRetVal::SUCCESS;
|
||||
}
|
||||
|
||||
GCMemcardImportFileRetVal GCMemcard::CopyFrom(const GCMemcard& source, u8 index)
|
||||
{
|
||||
if (!m_valid || !source.m_valid)
|
||||
return GCMemcardImportFileRetVal::NOMEMCARD;
|
||||
|
||||
std::optional<DEntry> tempDEntry = source.GetDEntry(index);
|
||||
if (!tempDEntry)
|
||||
return GCMemcardImportFileRetVal::NOMEMCARD;
|
||||
|
||||
u32 size = source.DEntry_BlockCount(index);
|
||||
if (size == 0xFFFF)
|
||||
return GCMemcardImportFileRetVal::INVALIDFILESIZE;
|
||||
|
||||
std::vector<GCMBlock> saveData;
|
||||
saveData.reserve(size);
|
||||
switch (source.GetSaveData(index, saveData))
|
||||
{
|
||||
case GCMemcardGetSaveDataRetVal::FAIL:
|
||||
return GCMemcardImportFileRetVal::FAIL;
|
||||
case GCMemcardGetSaveDataRetVal::NOMEMCARD:
|
||||
return GCMemcardImportFileRetVal::NOMEMCARD;
|
||||
default:
|
||||
FixChecksums();
|
||||
return ImportFile(*tempDEntry, saveData);
|
||||
}
|
||||
}
|
||||
|
||||
GCMemcardImportFileRetVal GCMemcard::ImportGci(const std::string& inputFile)
|
||||
{
|
||||
if (!m_valid)
|
||||
return GCMemcardImportFileRetVal::OPENFAIL;
|
||||
|
||||
File::IOFile gci(inputFile, "rb");
|
||||
if (!gci)
|
||||
return GCMemcardImportFileRetVal::OPENFAIL;
|
||||
|
||||
return ImportGciInternal(std::move(gci), inputFile);
|
||||
}
|
||||
|
||||
GCMemcardImportFileRetVal GCMemcard::ImportGciInternal(File::IOFile&& gci,
|
||||
const std::string& inputFile)
|
||||
{
|
||||
unsigned int offset;
|
||||
std::string fileType;
|
||||
SplitPath(inputFile, nullptr, nullptr, &fileType);
|
||||
|
||||
if (!strcasecmp(fileType.c_str(), ".gci"))
|
||||
offset = GCI;
|
||||
else
|
||||
{
|
||||
char tmp[0xD];
|
||||
gci.ReadBytes(tmp, sizeof(tmp));
|
||||
if (!strcasecmp(fileType.c_str(), ".gcs"))
|
||||
{
|
||||
if (!memcmp(tmp, "GCSAVE", 6)) // Header must be uppercase
|
||||
offset = GCS;
|
||||
else
|
||||
return GCMemcardImportFileRetVal::GCSFAIL;
|
||||
}
|
||||
else if (!strcasecmp(fileType.c_str(), ".sav"))
|
||||
{
|
||||
if (!memcmp(tmp, "DATELGC_SAVE", 0xC)) // Header must be uppercase
|
||||
offset = SAV;
|
||||
else
|
||||
return GCMemcardImportFileRetVal::SAVFAIL;
|
||||
}
|
||||
else
|
||||
return GCMemcardImportFileRetVal::OPENFAIL;
|
||||
}
|
||||
gci.Seek(offset, SEEK_SET);
|
||||
|
||||
DEntry tempDEntry;
|
||||
gci.ReadBytes(&tempDEntry, DENTRY_SIZE);
|
||||
const u64 fStart = gci.Tell();
|
||||
gci.Seek(0, SEEK_END);
|
||||
const u64 length = gci.Tell() - fStart;
|
||||
gci.Seek(offset + DENTRY_SIZE, SEEK_SET);
|
||||
|
||||
Gcs_SavConvert(tempDEntry, offset, length);
|
||||
|
||||
if (length != tempDEntry.m_block_count * BLOCK_SIZE)
|
||||
return GCMemcardImportFileRetVal::LENGTHFAIL;
|
||||
if (gci.Tell() != offset + DENTRY_SIZE) // Verify correct file position
|
||||
return GCMemcardImportFileRetVal::OPENFAIL;
|
||||
|
||||
u32 size = tempDEntry.m_block_count;
|
||||
std::vector<GCMBlock> saveData;
|
||||
saveData.reserve(size);
|
||||
|
||||
for (unsigned int i = 0; i < size; ++i)
|
||||
{
|
||||
GCMBlock b;
|
||||
gci.ReadBytes(b.m_block.data(), b.m_block.size());
|
||||
saveData.push_back(b);
|
||||
}
|
||||
return ImportFile(tempDEntry, saveData);
|
||||
}
|
||||
|
||||
GCMemcardExportFileRetVal GCMemcard::ExportGci(u8 index, const std::string& fileName,
|
||||
const std::string& directory) const
|
||||
{
|
||||
File::IOFile gci;
|
||||
int offset = GCI;
|
||||
|
||||
if (!fileName.length())
|
||||
{
|
||||
std::string gciFilename;
|
||||
// GCI_FileName should only fail if the gamecode is 0xFFFFFFFF
|
||||
if (!GCI_FileName(index, gciFilename))
|
||||
return GCMemcardExportFileRetVal::SUCCESS;
|
||||
gci.Open(directory + DIR_SEP + gciFilename, "wb");
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string fileType;
|
||||
gci.Open(fileName, "wb");
|
||||
SplitPath(fileName, nullptr, nullptr, &fileType);
|
||||
if (!strcasecmp(fileType.c_str(), ".gcs"))
|
||||
{
|
||||
offset = GCS;
|
||||
}
|
||||
else if (!strcasecmp(fileType.c_str(), ".sav"))
|
||||
{
|
||||
offset = SAV;
|
||||
}
|
||||
}
|
||||
|
||||
if (!gci)
|
||||
return GCMemcardExportFileRetVal::OPENFAIL;
|
||||
|
||||
gci.Seek(0, SEEK_SET);
|
||||
|
||||
switch (offset)
|
||||
{
|
||||
case GCS:
|
||||
u8 gcsHDR[GCS];
|
||||
memset(gcsHDR, 0, GCS);
|
||||
memcpy(gcsHDR, "GCSAVE", 6);
|
||||
gci.WriteArray(gcsHDR, GCS);
|
||||
break;
|
||||
|
||||
case SAV:
|
||||
u8 savHDR[SAV];
|
||||
memset(savHDR, 0, SAV);
|
||||
memcpy(savHDR, "DATELGC_SAVE", 0xC);
|
||||
gci.WriteArray(savHDR, SAV);
|
||||
break;
|
||||
}
|
||||
|
||||
std::optional<DEntry> tempDEntry = GetDEntry(index);
|
||||
if (!tempDEntry)
|
||||
return GCMemcardExportFileRetVal::NOMEMCARD;
|
||||
|
||||
Gcs_SavConvert(*tempDEntry, offset);
|
||||
gci.WriteBytes(&tempDEntry.value(), DENTRY_SIZE);
|
||||
|
||||
u32 size = DEntry_BlockCount(index);
|
||||
if (size == 0xFFFF)
|
||||
{
|
||||
return GCMemcardExportFileRetVal::FAIL;
|
||||
}
|
||||
|
||||
std::vector<GCMBlock> saveData;
|
||||
saveData.reserve(size);
|
||||
|
||||
switch (GetSaveData(index, saveData))
|
||||
{
|
||||
case GCMemcardGetSaveDataRetVal::FAIL:
|
||||
return GCMemcardExportFileRetVal::FAIL;
|
||||
case GCMemcardGetSaveDataRetVal::NOMEMCARD:
|
||||
return GCMemcardExportFileRetVal::NOMEMCARD;
|
||||
case GCMemcardGetSaveDataRetVal::SUCCESS:
|
||||
break;
|
||||
}
|
||||
gci.Seek(DENTRY_SIZE + offset, SEEK_SET);
|
||||
for (unsigned int i = 0; i < size; ++i)
|
||||
{
|
||||
gci.WriteBytes(saveData[i].m_block.data(), saveData[i].m_block.size());
|
||||
}
|
||||
|
||||
if (gci.IsGood())
|
||||
return GCMemcardExportFileRetVal::SUCCESS;
|
||||
else
|
||||
return GCMemcardExportFileRetVal::WRITEFAIL;
|
||||
}
|
||||
|
||||
void GCMemcard::Gcs_SavConvert(DEntry& tempDEntry, int saveType, u64 length)
|
||||
{
|
||||
switch (saveType)
|
||||
{
|
||||
case GCS:
|
||||
{
|
||||
// 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"
|
||||
tempDEntry.m_block_count = length / BLOCK_SIZE;
|
||||
}
|
||||
break;
|
||||
case SAV:
|
||||
// swap byte pairs
|
||||
// 0x2C and 0x2D, 0x2E and 0x2F, 0x30 and 0x31, 0x32 and 0x33,
|
||||
// 0x34 and 0x35, 0x36 and 0x37, 0x38 and 0x39, 0x3A and 0x3B,
|
||||
// 0x3C and 0x3D,0x3E and 0x3F.
|
||||
// It seems that sav files also swap the banner/icon flags...
|
||||
std::swap(tempDEntry.m_unused_1, tempDEntry.m_banner_and_icon_flags);
|
||||
|
||||
std::array<u8, 4> tmp;
|
||||
std::memcpy(tmp.data(), &tempDEntry.m_image_offset, 4);
|
||||
std::swap(tmp[0], tmp[1]);
|
||||
std::swap(tmp[2], tmp[3]);
|
||||
std::memcpy(&tempDEntry.m_image_offset, tmp.data(), 4);
|
||||
|
||||
std::memcpy(tmp.data(), &tempDEntry.m_icon_format, 2);
|
||||
std::swap(tmp[0], tmp[1]);
|
||||
std::memcpy(&tempDEntry.m_icon_format, tmp.data(), 2);
|
||||
|
||||
std::memcpy(tmp.data(), &tempDEntry.m_animation_speed, 2);
|
||||
std::swap(tmp[0], tmp[1]);
|
||||
std::memcpy(&tempDEntry.m_animation_speed, tmp.data(), 2);
|
||||
|
||||
std::swap(tempDEntry.m_file_permissions, tempDEntry.m_copy_counter);
|
||||
|
||||
std::memcpy(tmp.data(), &tempDEntry.m_first_block, 2);
|
||||
std::swap(tmp[0], tmp[1]);
|
||||
std::memcpy(&tempDEntry.m_first_block, tmp.data(), 2);
|
||||
|
||||
std::memcpy(tmp.data(), &tempDEntry.m_block_count, 2);
|
||||
std::swap(tmp[0], tmp[1]);
|
||||
std::memcpy(&tempDEntry.m_block_count, tmp.data(), 2);
|
||||
|
||||
std::swap(tempDEntry.m_unused_2[0], tempDEntry.m_unused_2[1]);
|
||||
|
||||
std::memcpy(tmp.data(), &tempDEntry.m_comments_address, 4);
|
||||
std::swap(tmp[0], tmp[1]);
|
||||
std::swap(tmp[2], tmp[3]);
|
||||
std::memcpy(&tempDEntry.m_comments_address, tmp.data(), 4);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::vector<u32>> GCMemcard::ReadBannerRGBA8(u8 index) const
|
||||
{
|
||||
if (!m_valid || index >= DIRLEN)
|
||||
|
||||
@@ -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 @@ private:
|
||||
|
||||
GCMemcard();
|
||||
|
||||
GCMemcardImportFileRetVal ImportGciInternal(File::IOFile&& gci, const std::string& inputFile);
|
||||
|
||||
const Directory& GetActiveDirectory() const;
|
||||
const BlockAlloc& GetActiveBat() const;
|
||||
|
||||
@@ -496,26 +481,15 @@ public:
|
||||
|
||||
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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 @@ public:
|
||||
~GCMemcardManager();
|
||||
|
||||
static QString GetErrorMessagesForErrorCode(const Memcard::GCMemcardErrorCode& code);
|
||||
static QString GetErrorMessageForErrorCode(Memcard::ReadSavefileErrorCode code);
|
||||
|
||||
private:
|
||||
struct IconAnimationData;
|
||||
@@ -51,11 +59,14 @@ private:
|
||||
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 @@ private:
|
||||
// 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;
|
||||
|
||||
Reference in New Issue
Block a user