diff --git a/Source/Core/Common/VariantUtil.h b/Source/Core/Common/VariantUtil.h index e9e2987d9c..a7c962e9f8 100644 --- a/Source/Core/Common/VariantUtil.h +++ b/Source/Core/Common/VariantUtil.h @@ -26,3 +26,12 @@ auto VariantCast(const std::variant& v) { return detail::VariantCastProxy{v}; } + +template +struct overloaded : Ts... +{ + using Ts::operator()...; +}; + +template +overloaded(Ts...) -> overloaded; diff --git a/Source/Core/Core/HW/GCMemcard/GCMemcard.cpp b/Source/Core/Core/HW/GCMemcard/GCMemcard.cpp index 27382a8a48..931db7d119 100644 --- a/Source/Core/Core/HW/GCMemcard/GCMemcard.cpp +++ b/Source/Core/Core/HW/GCMemcard/GCMemcard.cpp @@ -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& 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 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 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 tempDEntry = source.GetDEntry(index); - if (!tempDEntry) - return GCMemcardImportFileRetVal::NOMEMCARD; - - u32 size = source.DEntry_BlockCount(index); - if (size == 0xFFFF) - return GCMemcardImportFileRetVal::INVALIDFILESIZE; - - std::vector 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 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 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 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 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> GCMemcard::ReadBannerRGBA8(u8 index) const { if (!m_valid || index >= DIRLEN) diff --git a/Source/Core/Core/HW/GCMemcard/GCMemcard.h b/Source/Core/Core/HW/GCMemcard/GCMemcard.h index 7af8e0dfde..0cea51a017 100644 --- a/Source/Core/Core/HW/GCMemcard/GCMemcard.h +++ b/Source/Core/Core/HW/GCMemcard/GCMemcard.h @@ -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); #pragma pack(pop) +struct Savefile +{ + DEntry dir_entry; + std::vector 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& saveBlocks) const; - // adds the file to the directory and copies its contents - GCMemcardImportFileRetVal ImportFile(const DEntry& direntry, std::vector& 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 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> ReadBannerRGBA8(u8 index) const; diff --git a/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.cpp b/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.cpp index fb93096dbd..9699eddf2b 100644 --- a/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.cpp +++ b/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.cpp @@ -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); } } } diff --git a/Source/Core/Core/HW/GCMemcard/GCMemcardUtils.cpp b/Source/Core/Core/HW/GCMemcard/GCMemcardUtils.cpp index 3230e86339..4d32200ee4 100644 --- a/Source/Core/Core/HW/GCMemcard/GCMemcardUtils.cpp +++ b/Source/Core/Core/HW/GCMemcard/GCMemcardUtils.cpp @@ -1,9 +1,27 @@ #include "Core/HW/GCMemcard/GCMemcardUtils.h" +#include +#include +#include +#include + +#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 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 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& 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& 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& sav_header) +{ + std::array 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& sav_header, + const DEntry& dir_entry) +{ + std::array 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& 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 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 ReadSavefileInternalGCS(File::IOFile& file, + u64 filesize) +{ + std::array 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(block_count); + + if (!ReadBlocksFromIOFile(file, savefile.blocks, block_count)) + return ReadSavefileErrorCode::IOError; + + return savefile; +} + +static std::variant ReadSavefileInternalSAV(File::IOFile& file, + u64 filesize) +{ + std::array 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 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 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 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(entry.m_makercode.data()), + entry.m_makercode.size()); + std::string gamecode(reinterpret_cast(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(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 GetSavefiles(const GCMemcard& card, const std::vector& file_indices) +{ + std::vector files; + files.reserve(file_indices.size()); + for (const u8 index : file_indices) + { + std::optional file = card.ExportFile(index); + if (!file) + return {}; + files.emplace_back(std::move(*file)); + } + return files; +} + +size_t GetBlockCount(const std::vector& savefiles) +{ + size_t block_count = 0; + for (const Savefile& savefile : savefiles) + block_count += savefile.blocks.size(); + return block_count; +} } // namespace Memcard diff --git a/Source/Core/Core/HW/GCMemcard/GCMemcardUtils.h b/Source/Core/Core/HW/GCMemcard/GCMemcardUtils.h index ebe0daf4fa..0178feca42 100644 --- a/Source/Core/Core/HW/GCMemcard/GCMemcardUtils.h +++ b/Source/Core/Core/HW/GCMemcard/GCMemcardUtils.h @@ -4,9 +4,48 @@ #pragma once +#include +#include + +#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& 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 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 GetSavefiles(const GCMemcard& card, const std::vector& file_indices); + +// Gets the total amount of blocks the given saves use. +size_t GetBlockCount(const std::vector& savefiles); } // namespace Memcard diff --git a/Source/Core/DolphinQt/GCMemcardManager.cpp b/Source/Core/DolphinQt/GCMemcardManager.cpp index c66ee3c10f..e8a4e2edd5 100644 --- a/Source/Core/DolphinQt/GCMemcardManager.cpp +++ b/Source/Core/DolphinQt/GCMemcardManager.cpp @@ -5,6 +5,11 @@ #include "DolphinQt/GCMemcardManager.h" #include +#include +#include +#include + +#include #include #include @@ -15,25 +20,41 @@ #include #include #include +#include #include #include #include #include #include #include +#include +#include "Common/CommonPaths.h" #include "Common/Config/Config.h" #include "Common/FileUtil.h" #include "Common/MsgHandler.h" #include "Common/StringUtil.h" +#include "Common/VariantUtil.h" #include "Core/Config/MainSettings.h" #include "Core/HW/GCMemcard/GCMemcard.h" +#include "Core/HW/GCMemcard/GCMemcardUtils.h" #include "DolphinQt/GCMemcardCreateNewDialog.h" #include "DolphinQt/QtUtils/ModalMessageBox.h" -constexpr float ROW_HEIGHT = 28; +constexpr int ROW_HEIGHT = 36; +constexpr int COLUMN_WIDTH_FILENAME = 100; +constexpr int COLUMN_WIDTH_BANNER = Memcard::MEMORY_CARD_BANNER_WIDTH + 6; +constexpr int COLUMN_WIDTH_TEXT = 160; +constexpr int COLUMN_WIDTH_ICON = Memcard::MEMORY_CARD_ICON_WIDTH + 6; +constexpr int COLUMN_WIDTH_BLOCKS = 40; +constexpr int COLUMN_INDEX_FILENAME = 0; +constexpr int COLUMN_INDEX_BANNER = 1; +constexpr int COLUMN_INDEX_TEXT = 2; +constexpr int COLUMN_INDEX_ICON = 3; +constexpr int COLUMN_INDEX_BLOCKS = 4; +constexpr int COLUMN_COUNT = 5; struct GCMemcardManager::IconAnimationData { @@ -78,11 +99,20 @@ void GCMemcardManager::CreateWidgets() // Actions m_select_button = new QPushButton; m_copy_button = new QPushButton; - - // Contents will be set by their appropriate functions m_delete_button = new QPushButton(tr("&Delete")); - m_export_button = new QPushButton(tr("&Export...")); - m_export_all_button = new QPushButton(tr("Export &All...")); + + m_export_button = new QToolButton(this); + m_export_menu = new QMenu(m_export_button); + m_export_gci_action = new QAction(tr("&Export as .gci..."), m_export_menu); + m_export_gcs_action = new QAction(tr("Export as .&gcs..."), m_export_menu); + m_export_sav_action = new QAction(tr("Export as .&sav..."), m_export_menu); + m_export_menu->addAction(m_export_gci_action); + m_export_menu->addAction(m_export_gcs_action); + m_export_menu->addAction(m_export_sav_action); + m_export_button->setDefaultAction(m_export_gci_action); + m_export_button->setPopupMode(QToolButton::MenuButtonPopup); + m_export_button->setMenu(m_export_menu); + m_import_button = new QPushButton(tr("&Import...")); m_fix_checksums_button = new QPushButton(tr("Fix Checksums")); @@ -100,8 +130,25 @@ void GCMemcardManager::CreateWidgets() m_slot_table[i]->setSelectionMode(QAbstractItemView::ExtendedSelection); m_slot_table[i]->setSelectionBehavior(QAbstractItemView::SelectRows); - m_slot_table[i]->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + m_slot_table[i]->setSortingEnabled(true); m_slot_table[i]->horizontalHeader()->setHighlightSections(false); + m_slot_table[i]->horizontalHeader()->setMinimumSectionSize(0); + m_slot_table[i]->horizontalHeader()->setSortIndicatorShown(true); + m_slot_table[i]->setColumnCount(COLUMN_COUNT); + m_slot_table[i]->setHorizontalHeaderItem(COLUMN_INDEX_FILENAME, + new QTableWidgetItem(tr("Filename"))); + m_slot_table[i]->setHorizontalHeaderItem(COLUMN_INDEX_BANNER, + new QTableWidgetItem(tr("Banner"))); + m_slot_table[i]->setHorizontalHeaderItem(COLUMN_INDEX_TEXT, new QTableWidgetItem(tr("Title"))); + m_slot_table[i]->setHorizontalHeaderItem(COLUMN_INDEX_ICON, new QTableWidgetItem(tr("Icon"))); + m_slot_table[i]->setHorizontalHeaderItem(COLUMN_INDEX_BLOCKS, + new QTableWidgetItem(tr("Blocks"))); + m_slot_table[i]->setColumnWidth(COLUMN_INDEX_FILENAME, COLUMN_WIDTH_FILENAME); + m_slot_table[i]->setColumnWidth(COLUMN_INDEX_BANNER, COLUMN_WIDTH_BANNER); + m_slot_table[i]->setColumnWidth(COLUMN_INDEX_TEXT, COLUMN_WIDTH_TEXT); + m_slot_table[i]->setColumnWidth(COLUMN_INDEX_ICON, COLUMN_WIDTH_ICON); + m_slot_table[i]->setColumnWidth(COLUMN_INDEX_BLOCKS, COLUMN_WIDTH_BLOCKS); + m_slot_table[i]->verticalHeader()->setDefaultSectionSize(ROW_HEIGHT); m_slot_table[i]->verticalHeader()->hide(); m_slot_table[i]->setShowGrid(false); @@ -114,7 +161,7 @@ void GCMemcardManager::CreateWidgets() slot_layout->addWidget(m_slot_table[i], 1, 0, 1, 3); slot_layout->addWidget(m_slot_stat_label[i], 2, 0); - layout->addWidget(m_slot_group[i], 0, i * 2, 9, 1); + layout->addWidget(m_slot_group[i], 0, i * 2, 8, 1); UpdateSlotTable(i); } @@ -123,10 +170,9 @@ void GCMemcardManager::CreateWidgets() layout->addWidget(m_copy_button, 2, 1); layout->addWidget(m_delete_button, 3, 1); layout->addWidget(m_export_button, 4, 1); - layout->addWidget(m_export_all_button, 5, 1); - layout->addWidget(m_import_button, 6, 1); - layout->addWidget(m_fix_checksums_button, 7, 1); - layout->addWidget(m_button_box, 9, 2); + layout->addWidget(m_import_button, 5, 1); + layout->addWidget(m_fix_checksums_button, 6, 1); + layout->addWidget(m_button_box, 8, 2); setLayout(layout); } @@ -135,8 +181,12 @@ void GCMemcardManager::ConnectWidgets() { connect(m_button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(m_select_button, &QPushButton::clicked, [this] { SetActiveSlot(!m_active_slot); }); - connect(m_export_button, &QPushButton::clicked, [this] { ExportFiles(true); }); - connect(m_export_all_button, &QPushButton::clicked, this, &GCMemcardManager::ExportAllFiles); + connect(m_export_gci_action, &QAction::triggered, + [this] { ExportFiles(Memcard::SavefileFormat::GCI); }); + connect(m_export_gcs_action, &QAction::triggered, + [this] { ExportFiles(Memcard::SavefileFormat::GCS); }); + connect(m_export_sav_action, &QAction::triggered, + [this] { ExportFiles(Memcard::SavefileFormat::SAV); }); connect(m_delete_button, &QPushButton::clicked, this, &GCMemcardManager::DeleteFiles); connect(m_import_button, &QPushButton::clicked, this, &GCMemcardManager::ImportFile); connect(m_copy_button, &QPushButton::clicked, this, &GCMemcardManager::CopyFiles); @@ -188,67 +238,69 @@ void GCMemcardManager::SetActiveSlot(int slot) void GCMemcardManager::UpdateSlotTable(int slot) { m_slot_active_icons[slot].clear(); - m_slot_table[slot]->clear(); - m_slot_table[slot]->setColumnCount(6); - m_slot_table[slot]->verticalHeader()->setDefaultSectionSize(ROW_HEIGHT); - m_slot_table[slot]->verticalHeader()->setDefaultSectionSize(QHeaderView::Fixed); - m_slot_table[slot]->setHorizontalHeaderLabels( - {tr("Banner"), tr("Title"), tr("Comment"), tr("Icon"), tr("Blocks"), tr("First Block")}); if (m_slot_memcard[slot] == nullptr) + { + m_slot_table[slot]->setRowCount(0); + m_slot_stat_label[slot]->clear(); return; + } auto& memcard = m_slot_memcard[slot]; auto* table = m_slot_table[slot]; - - const auto create_item = [](const QString& string = {}) { - QTableWidgetItem* item = new QTableWidgetItem(string); - item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); - return item; - }; + table->setSortingEnabled(false); const u8 num_files = memcard->GetNumFiles(); - m_slot_active_icons[slot].reserve(num_files); + const u8 free_files = Memcard::DIRLEN - num_files; + const u16 free_blocks = memcard->GetFreeBlocks(); + table->setRowCount(num_files); for (int i = 0; i < num_files; i++) { - int file_index = memcard->GetFileIndex(i); - table->setRowCount(i + 1); + const u8 file_index = memcard->GetFileIndex(i); const auto file_comments = memcard->GetSaveComments(file_index); + const u16 block_count = memcard->DEntry_BlockCount(file_index); + const auto entry = memcard->GetDEntry(file_index); + const std::string filename = entry ? Memcard::GenerateFilename(*entry) : ""; - QString title; - QString comment; - if (file_comments) + const QString title = + file_comments ? QString::fromStdString(file_comments->first).trimmed() : QString(); + const QString comment = + file_comments ? QString::fromStdString(file_comments->second).trimmed() : QString(); + auto banner = GetBannerFromSaveFile(file_index, slot); + auto icon_data = GetIconFromSaveFile(file_index, slot); + + auto* item_filename = new QTableWidgetItem(QString::fromStdString(filename)); + auto* item_banner = new QTableWidgetItem(); + auto* item_text = new QTableWidgetItem(QStringLiteral("%1\n%2").arg(title, comment)); + auto* item_icon = new QTableWidgetItem(); + auto* item_blocks = new QTableWidgetItem(); + + item_banner->setData(Qt::DecorationRole, banner); + item_icon->setData(Qt::DecorationRole, icon_data.m_frames[0]); + item_blocks->setData(Qt::DisplayRole, block_count); + + for (auto* item : {item_filename, item_banner, item_text, item_icon, item_blocks}) { - title = QString::fromStdString(file_comments->first); - comment = QString::fromStdString(file_comments->second); + item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); + item->setData(Qt::UserRole, static_cast(file_index)); } - QString blocks = QStringLiteral("%1").arg(memcard->DEntry_BlockCount(file_index)); - QString block_count = QStringLiteral("%1").arg(memcard->DEntry_FirstBlock(file_index)); + m_slot_active_icons[slot].emplace(file_index, std::move(icon_data)); - auto* banner = new QTableWidgetItem; - banner->setData(Qt::DecorationRole, GetBannerFromSaveFile(file_index, slot)); - banner->setFlags(banner->flags() ^ Qt::ItemIsEditable); - - auto icon_data = GetIconFromSaveFile(file_index, slot); - auto* icon = new QTableWidgetItem; - icon->setData(Qt::DecorationRole, icon_data.m_frames[0]); - - m_slot_active_icons[slot].emplace_back(std::move(icon_data)); - - table->setItem(i, 0, banner); - table->setItem(i, 1, create_item(title)); - table->setItem(i, 2, create_item(comment)); - table->setItem(i, 3, icon); - table->setItem(i, 4, create_item(blocks)); - table->setItem(i, 5, create_item(block_count)); - table->resizeRowToContents(i); + table->setItem(i, COLUMN_INDEX_FILENAME, item_filename); + table->setItem(i, COLUMN_INDEX_BANNER, item_banner); + table->setItem(i, COLUMN_INDEX_TEXT, item_text); + table->setItem(i, COLUMN_INDEX_ICON, item_icon); + table->setItem(i, COLUMN_INDEX_BLOCKS, item_blocks); } - m_slot_stat_label[slot]->setText(tr("%1 Free Blocks; %2 Free Dir Entries") - .arg(memcard->GetFreeBlocks()) - .arg(Memcard::DIRLEN - memcard->GetNumFiles())); + const QString free_blocks_string = tr("Free Blocks: %1").arg(free_blocks); + const QString free_files_string = tr("Free Files: %1").arg(free_files); + m_slot_stat_label[slot]->setText( + QStringLiteral("%1 %2").arg(free_blocks_string, free_files_string)); + + table->setSortingEnabled(true); } void GCMemcardManager::UpdateActions() @@ -260,7 +312,6 @@ void GCMemcardManager::UpdateActions() m_copy_button->setEnabled(have_selection && have_memcard_other); m_export_button->setEnabled(have_selection); - m_export_all_button->setEnabled(have_memcard); m_import_button->setEnabled(have_memcard); m_delete_button->setEnabled(have_selection); m_fix_checksums_button->setEnabled(have_memcard); @@ -278,7 +329,7 @@ void GCMemcardManager::SetSlotFile(int slot, QString path) else { m_slot_memcard[slot] = nullptr; - ModalMessageBox::critical( + ModalMessageBox::warning( this, tr("Error"), tr("Failed opening memory card:\n%1").arg(GetErrorMessagesForErrorCode(error_code))); } @@ -293,162 +344,322 @@ void GCMemcardManager::SetSlotFileInteractive(int slot) this, slot == 0 ? tr("Set memory card file for Slot A") : tr("Set memory card file for Slot B"), QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)), - tr("GameCube Memory Cards (*.raw *.gcp)") + QStringLiteral(";;") + tr("All Files (*)"))); + QStringLiteral("%1 (*.raw *.gcp);;%2 (*)") + .arg(tr("GameCube Memory Cards"), tr("All Files")))); if (!path.isEmpty()) m_slot_file_edit[slot]->setText(path); } -void GCMemcardManager::ExportFiles(bool prompt) +std::vector GCMemcardManager::GetSelectedFileIndices() { - auto selection = m_slot_table[m_active_slot]->selectedItems(); - auto& memcard = m_slot_memcard[m_active_slot]; - - auto count = selection.count() / m_slot_table[m_active_slot]->columnCount(); - - for (int i = 0; i < count; i++) + const auto selection = m_slot_table[m_active_slot]->selectedItems(); + std::vector lookup(Memcard::DIRLEN); + for (const auto* item : selection) { - auto sel = selection[i * m_slot_table[m_active_slot]->columnCount()]; - int file_index = memcard->GetFileIndex(m_slot_table[m_active_slot]->row(sel)); + const int index = item->data(Qt::UserRole).toInt(); + if (index < 0 || index >= static_cast(Memcard::DIRLEN)) + { + ModalMessageBox::warning(this, tr("Error"), + tr("Data inconsistency in GCMemcardManager, aborting action.")); + return {}; + } + lookup[index] = true; + } - std::string gci_filename; - if (!memcard->GCI_FileName(file_index, gci_filename)) + std::vector selected_indices; + for (u8 i = 0; i < Memcard::DIRLEN; ++i) + { + if (lookup[i]) + selected_indices.push_back(i); + } + + return selected_indices; +} + +static QString GetFormatDescription(Memcard::SavefileFormat format) +{ + switch (format) + { + case Memcard::SavefileFormat::GCI: + return QObject::tr("Native GCI File"); + case Memcard::SavefileFormat::GCS: + return QObject::tr("MadCatz Gameshark files"); + case Memcard::SavefileFormat::SAV: + return QObject::tr("Datel MaxDrive/Pro files"); + default: + assert(0); + return QObject::tr("Native GCI File"); + } +} + +void GCMemcardManager::ExportFiles(Memcard::SavefileFormat format) +{ + const auto& memcard = m_slot_memcard[m_active_slot]; + if (!memcard) + return; + + const auto selected_indices = GetSelectedFileIndices(); + if (selected_indices.empty()) + return; + + const auto savefiles = Memcard::GetSavefiles(*memcard, selected_indices); + if (savefiles.empty()) + { + ModalMessageBox::warning(this, tr("Export Failed"), + tr("Failed to read selected savefile(s) from memory card.")); + return; + } + + std::string extension = Memcard::GetDefaultExtension(format); + + if (savefiles.size() == 1) + { + // when exporting a single save file, let user specify exact path + const std::string basename = Memcard::GenerateFilename(savefiles[0].dir_entry); + const QString qformatdesc = GetFormatDescription(format); + const std::string default_path = + fmt::format("{}/{}{}", File::GetUserPath(D_GCUSER_IDX), basename, extension); + const QString qfilename = QFileDialog::getSaveFileName( + this, tr("Export Save File"), QString::fromStdString(default_path), + QStringLiteral("%1 (*%2);;%3 (*)") + .arg(qformatdesc, QString::fromStdString(extension), tr("All Files"))); + if (qfilename.isEmpty()) return; - QString path; - if (prompt) + const std::string filename = qfilename.toStdString(); + if (!Memcard::WriteSavefile(filename, savefiles[0], format)) { - path = QFileDialog::getSaveFileName( - this, tr("Export Save File"), - QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)) + - QStringLiteral("/%1").arg(QString::fromStdString(gci_filename)), - tr("Native GCI File (*.gci)") + QStringLiteral(";;") + - tr("MadCatz Gameshark files(*.gcs)") + QStringLiteral(";;") + - tr("Datel MaxDrive/Pro files(*.sav)")); - - if (path.isEmpty()) - return; - } - else - { - path = QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)) + - QStringLiteral("/%1").arg(QString::fromStdString(gci_filename)); + File::Delete(filename); + ModalMessageBox::warning(this, tr("Export Failed"), tr("Failed to write savefile to disk.")); } - // TODO: This is obviously intended to check for success instead. - const auto exportRetval = memcard->ExportGci(file_index, path.toStdString(), ""); - if (exportRetval == Memcard::GCMemcardExportFileRetVal::UNUSED) + return; + } + + const QString qdirpath = + QFileDialog::getExistingDirectory(this, QObject::tr("Export Save Files"), + QString::fromStdString(File::GetUserPath(D_GCUSER_IDX))); + if (qdirpath.isEmpty()) + return; + + const std::string dirpath = qdirpath.toStdString(); + size_t failures = 0; + for (const auto& savefile : savefiles) + { + // find a free filename so we don't overwrite anything + const std::string basepath = dirpath + DIR_SEP + Memcard::GenerateFilename(savefile.dir_entry); + std::string filename = basepath + extension; + if (File::Exists(filename)) { - File::Delete(path.toStdString()); + size_t tmp = 0; + std::string free_name; + do + { + free_name = fmt::format("{}_{}{}", basepath, tmp, extension); + ++tmp; + } while (File::Exists(free_name)); + filename = free_name; + } + + if (!Memcard::WriteSavefile(filename, savefile, format)) + { + File::Delete(filename); + ++failures; } } - QString text = count == 1 ? tr("Successfully exported the save file.") : - tr("Successfully exported the %1 save files.").arg(count); - ModalMessageBox::information(this, tr("Success"), text); + if (failures > 0) + { + QString failure_string = + tr("Failed to export %n out of %1 save file(s).", "", static_cast(failures)) + .arg(savefiles.size()); + if (failures == savefiles.size()) + { + ModalMessageBox::warning(this, tr("Export Failed"), failure_string); + } + else + { + QString success_string = tr("Successfully exported %n out of %1 save file(s).", "", + static_cast(savefiles.size() - failures)) + .arg(savefiles.size()); + ModalMessageBox::warning(this, tr("Export Failed"), + QStringLiteral("%1\n%2").arg(failure_string, success_string)); + } + } } -void GCMemcardManager::ExportAllFiles() +void GCMemcardManager::ImportFiles(int slot, const std::vector& savefiles) { - // This is nothing but a thin wrapper around ExportFiles() - m_slot_table[m_active_slot]->selectAll(); - ExportFiles(false); + auto& card = m_slot_memcard[slot]; + if (!card) + return; + + const size_t number_of_files = savefiles.size(); + const size_t number_of_blocks = Memcard::GetBlockCount(savefiles); + const size_t free_files = Memcard::DIRLEN - card->GetNumFiles(); + const size_t free_blocks = card->GetFreeBlocks(); + + QStringList error_messages; + + if (number_of_files > free_files) + { + error_messages.push_back( + tr("Not enough free files on the target memory card. At least %n free file(s) required.", + "", static_cast(number_of_files))); + } + + if (number_of_blocks > free_blocks) + { + error_messages.push_back( + tr("Not enough free blocks on the target memory card. At least %n free block(s) required.", + "", static_cast(number_of_blocks))); + } + + if (Memcard::HasDuplicateIdentity(savefiles)) + { + error_messages.push_back( + tr("At least two of the selected save files have the same internal filename.")); + } + + for (const Memcard::Savefile& savefile : savefiles) + { + if (card->TitlePresent(savefile.dir_entry)) + { + const std::string filename = Memcard::GenerateFilename(savefile.dir_entry); + error_messages.push_back(tr("The target memory card already contains a file \"%1\".") + .arg(QString::fromStdString(filename))); + } + } + + if (!error_messages.empty()) + { + ModalMessageBox::warning(this, tr("Import Failed"), error_messages.join(QLatin1Char('\n'))); + return; + } + + for (const Memcard::Savefile& savefile : savefiles) + { + const auto result = card->ImportFile(savefile); + + // we've already checked everything that could realistically fail here, so this should only + // happen if the memory card data is corrupted in some way + if (result != Memcard::GCMemcardImportFileRetVal::SUCCESS) + { + const std::string filename = Memcard::GenerateFilename(savefile.dir_entry); + ModalMessageBox::warning( + this, tr("Import Failed"), + tr("Failed to import \"%1\".").arg(QString::fromStdString(filename))); + break; + } + } + + if (!card->Save()) + { + ModalMessageBox::warning(this, tr("Import Failed"), + tr("Failed to write modified memory card to disk.")); + } + + UpdateSlotTable(slot); } void GCMemcardManager::ImportFile() { - QString path = QFileDialog::getOpenFileName( - this, tr("Import Save File"), QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)), - tr("Native GCI File (*.gci)") + QStringLiteral(";;") + tr("MadCatz Gameshark files(*.gcs)") + - QStringLiteral(";;") + tr("Datel MaxDrive/Pro files(*.sav)")); - - if (path.isEmpty()) + auto& card = m_slot_memcard[m_active_slot]; + if (!card) return; - const auto result = m_slot_memcard[m_active_slot]->ImportGci(path.toStdString()); + const QStringList paths = QFileDialog::getOpenFileNames( + this, tr("Import Save File(s)"), QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)), + QStringLiteral("%1 (*.gci *.gcs *.sav);;%2 (*.gci);;%3 (*.gcs);;%4 (*.sav);;%5 (*)") + .arg(tr("Supported file formats"), GetFormatDescription(Memcard::SavefileFormat::GCI), + GetFormatDescription(Memcard::SavefileFormat::GCS), + GetFormatDescription(Memcard::SavefileFormat::SAV), tr("All Files"))); - if (result != Memcard::GCMemcardImportFileRetVal::SUCCESS) + if (paths.isEmpty()) + return; + + std::vector savefiles; + savefiles.reserve(paths.size()); + QStringList errors; + for (const QString& path : paths) { - ModalMessageBox::critical(this, tr("Import failed"), tr("Failed to import \"%1\".").arg(path)); + auto read_result = Memcard::ReadSavefile(path.toStdString()); + std::visit(overloaded{ + [&](Memcard::Savefile savefile) { savefiles.emplace_back(std::move(savefile)); }, + [&](Memcard::ReadSavefileErrorCode error_code) { + errors.push_back( + tr("%1: %2").arg(path, GetErrorMessageForErrorCode(error_code))); + }, + }, + std::move(read_result)); + } + + if (!errors.empty()) + { + ModalMessageBox::warning( + this, tr("Import Failed"), + tr("Encountered the following errors while opening save files:\n%1\n\nAborting import.") + .arg(errors.join(QStringLiteral("\n")))); return; } - if (!m_slot_memcard[m_active_slot]->Save()) - PanicAlertFmtT("File write failed"); - - UpdateSlotTable(m_active_slot); + ImportFiles(m_active_slot, savefiles); } void GCMemcardManager::CopyFiles() { - auto selection = m_slot_table[m_active_slot]->selectedItems(); - auto& memcard = m_slot_memcard[m_active_slot]; + const auto& source_card = m_slot_memcard[m_active_slot]; + if (!source_card) + return; - auto count = selection.count() / m_slot_table[m_active_slot]->columnCount(); + auto& target_card = m_slot_memcard[!m_active_slot]; + if (!target_card) + return; - for (int i = 0; i < count; i++) + const auto selected_indices = GetSelectedFileIndices(); + if (selected_indices.empty()) + return; + + const auto savefiles = Memcard::GetSavefiles(*source_card, selected_indices); + if (savefiles.empty()) { - auto sel = selection[i * m_slot_table[m_active_slot]->columnCount()]; - int file_index = memcard->GetFileIndex(m_slot_table[m_active_slot]->row(sel)); - - const auto result = m_slot_memcard[!m_active_slot]->CopyFrom(*memcard, file_index); - - if (result != Memcard::GCMemcardImportFileRetVal::SUCCESS) - { - ModalMessageBox::warning(this, tr("Copy failed"), tr("Failed to copy file")); - } + ModalMessageBox::warning(this, tr("Copy Failed"), + tr("Failed to read selected savefile(s) from memory card.")); + return; } - for (int i = 0; i < SLOT_COUNT; i++) - { - if (!m_slot_memcard[i]->Save()) - PanicAlertFmtT("File write failed"); - - UpdateSlotTable(i); - } + ImportFiles(!m_active_slot, savefiles); } void GCMemcardManager::DeleteFiles() { - auto selection = m_slot_table[m_active_slot]->selectedItems(); - auto& memcard = m_slot_memcard[m_active_slot]; + auto& card = m_slot_memcard[m_active_slot]; + if (!card) + return; - auto count = selection.count() / m_slot_table[m_active_slot]->columnCount(); + const auto selected_indices = GetSelectedFileIndices(); + if (selected_indices.empty()) + return; - // Ask for confirmation if we are to delete multiple files - if (count > 1) + const QString text = tr("Do you want to delete the %n selected save file(s)?", "", + static_cast(selected_indices.size())); + const auto response = ModalMessageBox::question(this, tr("Question"), text); + if (response != QMessageBox::Yes) + return; + + for (const u8 index : selected_indices) { - QString text = count == 1 ? tr("Do you want to delete the selected save file?") : - tr("Do you want to delete the %1 selected save files?").arg(count); - - auto response = ModalMessageBox::question(this, tr("Question"), text); - ; - - if (response == QMessageBox::Abort) - return; - } - - std::vector file_indices; - for (int i = 0; i < count; i++) - { - auto sel = selection[i * m_slot_table[m_active_slot]->columnCount()]; - file_indices.push_back(memcard->GetFileIndex(m_slot_table[m_active_slot]->row(sel))); - } - - for (int file_index : file_indices) - { - if (memcard->RemoveFile(file_index) != Memcard::GCMemcardRemoveFileRetVal::SUCCESS) + if (card->RemoveFile(index) != Memcard::GCMemcardRemoveFileRetVal::SUCCESS) { - ModalMessageBox::warning(this, tr("Remove failed"), tr("Failed to remove file")); + ModalMessageBox::warning(this, tr("Remove Failed"), tr("Failed to remove file.")); + break; } } - if (!memcard->Save()) + if (!card->Save()) { - PanicAlertFmtT("File write failed"); - } - else - { - ModalMessageBox::information(this, tr("Success"), tr("Successfully deleted files.")); + ModalMessageBox::warning(this, tr("Remove Failed"), + tr("Failed to write modified memory card to disk.")); } UpdateSlotTable(m_active_slot); @@ -461,7 +672,10 @@ void GCMemcardManager::FixChecksums() memcard->FixChecksums(); if (!memcard->Save()) - PanicAlertFmtT("File write failed"); + { + ModalMessageBox::warning(this, tr("Fix Checksums Failed"), + tr("Failed to write modified memory card to disk.")); + } } void GCMemcardManager::CreateNewCard(int slot) @@ -473,22 +687,37 @@ void GCMemcardManager::CreateNewCard(int slot) void GCMemcardManager::DrawIcons() { - const auto column = 3; + const int column = COLUMN_INDEX_ICON; for (int slot = 0; slot < SLOT_COUNT; slot++) { - // skip loop if the table is empty - if (m_slot_table[slot]->rowCount() <= 0) + QTableWidget* table = m_slot_table[slot]; + const int row_count = table->rowCount(); + + if (row_count <= 0) continue; - const auto viewport = m_slot_table[slot]->viewport(); - u32 row = m_slot_table[slot]->indexAt(viewport->rect().topLeft()).row(); - const auto max_table_index = m_slot_table[slot]->indexAt(viewport->rect().bottomLeft()); - const u32 max_row = - max_table_index.row() < 0 ? (m_slot_table[slot]->rowCount() - 1) : max_table_index.row(); + const auto viewport = table->viewport(); + const int viewport_first_row = table->indexAt(viewport->rect().topLeft()).row(); + if (viewport_first_row >= row_count) + continue; - for (; row <= max_row; row++) + const int first_row = viewport_first_row < 0 ? 0 : viewport_first_row; + const int viewport_last_row = table->indexAt(viewport->rect().bottomLeft()).row(); + const int last_row = + viewport_last_row < 0 ? (row_count - 1) : std::min(viewport_last_row, row_count - 1); + + for (int row = first_row; row <= last_row; ++row) { - const auto& icon = m_slot_active_icons[slot][row]; + auto* item = table->item(row, column); + if (!item) + continue; + + const u8 index = static_cast(item->data(Qt::UserRole).toInt()); + auto it = m_slot_active_icons[slot].find(index); + if (it == m_slot_active_icons[slot].end()) + continue; + + const auto& icon = it->second; // this icon doesn't have an animation if (icon.m_frames.size() <= 1) @@ -502,11 +731,7 @@ void GCMemcardManager::DrawIcons() if (prev_frame == current_frame) continue; - auto* item = new QTableWidgetItem; item->setData(Qt::DecorationRole, icon.m_frames[current_frame]); - item->setFlags(item->flags() ^ Qt::ItemIsEditable); - - m_slot_table[slot]->setItem(row, column, item); } } @@ -613,3 +838,18 @@ QString GCMemcardManager::GetErrorMessagesForErrorCode(const Memcard::GCMemcardE return sl.join(QLatin1Char{'\n'}); } + +QString GCMemcardManager::GetErrorMessageForErrorCode(Memcard::ReadSavefileErrorCode code) +{ + switch (code) + { + case Memcard::ReadSavefileErrorCode::OpenFileFail: + return tr("Failed to open file."); + case Memcard::ReadSavefileErrorCode::IOError: + return tr("Failed to read from file."); + case Memcard::ReadSavefileErrorCode::DataCorrupted: + return tr("Data in unrecognized format or corrupted."); + default: + return tr("Unknown error."); + } +} diff --git a/Source/Core/DolphinQt/GCMemcardManager.h b/Source/Core/DolphinQt/GCMemcardManager.h index c7e2202474..21f5004fbd 100644 --- a/Source/Core/DolphinQt/GCMemcardManager.h +++ b/Source/Core/DolphinQt/GCMemcardManager.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include #include @@ -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 GetSelectedFileIndices(); + + void ImportFiles(int slot, const std::vector& 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, SLOT_COUNT> m_slot_active_icons; + std::array, SLOT_COUNT> m_slot_active_icons; std::array, SLOT_COUNT> m_slot_memcard; std::array m_slot_group; std::array m_slot_file_edit;