mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-01-10 08:09:26 +01:00
Merge pull request #8996 from AdmiralCurtiss/memcard-save-import-export-refactor
Various improvements for the Memory Card Manager.
This commit is contained in:
commit
9843412440
@ -26,3 +26,12 @@ auto VariantCast(const std::variant<From...>& v)
|
|||||||
{
|
{
|
||||||
return detail::VariantCastProxy<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;
|
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
|
std::string GCMemcard::DEntry_GameCode(u8 index) const
|
||||||
{
|
{
|
||||||
if (!m_valid || index >= DIRLEN)
|
if (!m_valid || index >= DIRLEN)
|
||||||
@ -836,14 +833,14 @@ GCMemcardGetSaveDataRetVal GCMemcard::GetSaveData(u8 index, std::vector<GCMBlock
|
|||||||
}
|
}
|
||||||
return GCMemcardGetSaveDataRetVal::SUCCESS;
|
return GCMemcardGetSaveDataRetVal::SUCCESS;
|
||||||
}
|
}
|
||||||
// End DEntry functions
|
|
||||||
|
|
||||||
GCMemcardImportFileRetVal GCMemcard::ImportFile(const DEntry& direntry,
|
GCMemcardImportFileRetVal GCMemcard::ImportFile(const Savefile& savefile)
|
||||||
std::vector<GCMBlock>& saveBlocks)
|
|
||||||
{
|
{
|
||||||
if (!m_valid)
|
if (!m_valid)
|
||||||
return GCMemcardImportFileRetVal::NOMEMCARD;
|
return GCMemcardImportFileRetVal::NOMEMCARD;
|
||||||
|
|
||||||
|
const DEntry& direntry = savefile.dir_entry;
|
||||||
|
|
||||||
if (GetNumFiles() >= DIRLEN)
|
if (GetNumFiles() >= DIRLEN)
|
||||||
{
|
{
|
||||||
return GCMemcardImportFileRetVal::OUTOFDIRENTRIES;
|
return GCMemcardImportFileRetVal::OUTOFDIRENTRIES;
|
||||||
@ -880,8 +877,9 @@ GCMemcardImportFileRetVal GCMemcard::ImportFile(const DEntry& direntry,
|
|||||||
|
|
||||||
int fileBlocks = direntry.m_block_count;
|
int fileBlocks = direntry.m_block_count;
|
||||||
|
|
||||||
FZEROGX_MakeSaveGameValid(m_header_block, direntry, saveBlocks);
|
std::vector<GCMBlock> blocks = savefile.blocks;
|
||||||
PSO_MakeSaveGameValid(m_header_block, direntry, saveBlocks);
|
FZEROGX_MakeSaveGameValid(m_header_block, direntry, blocks);
|
||||||
|
PSO_MakeSaveGameValid(m_header_block, direntry, blocks);
|
||||||
|
|
||||||
BlockAlloc UpdatedBat = GetActiveBat();
|
BlockAlloc UpdatedBat = GetActiveBat();
|
||||||
u16 nextBlock;
|
u16 nextBlock;
|
||||||
@ -890,7 +888,7 @@ GCMemcardImportFileRetVal GCMemcard::ImportFile(const DEntry& direntry,
|
|||||||
{
|
{
|
||||||
if (firstBlock == 0xFFFF)
|
if (firstBlock == 0xFFFF)
|
||||||
PanicAlertFmt("Fatal Error");
|
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)
|
if (i == fileBlocks - 1)
|
||||||
nextBlock = 0xFFFF;
|
nextBlock = 0xFFFF;
|
||||||
else
|
else
|
||||||
@ -909,6 +907,22 @@ GCMemcardImportFileRetVal GCMemcard::ImportFile(const DEntry& direntry,
|
|||||||
return GCMemcardImportFileRetVal::SUCCESS;
|
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
|
GCMemcardRemoveFileRetVal GCMemcard::RemoveFile(u8 index) // index in the directory array
|
||||||
{
|
{
|
||||||
if (!m_valid)
|
if (!m_valid)
|
||||||
@ -940,250 +954,6 @@ GCMemcardRemoveFileRetVal GCMemcard::RemoveFile(u8 index) // index in the direc
|
|||||||
return GCMemcardRemoveFileRetVal::SUCCESS;
|
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
|
std::optional<std::vector<u32>> GCMemcard::ReadBannerRGBA8(u8 index) const
|
||||||
{
|
{
|
||||||
if (!m_valid || index >= DIRLEN)
|
if (!m_valid || index >= DIRLEN)
|
||||||
|
@ -30,9 +30,6 @@ enum
|
|||||||
{
|
{
|
||||||
SLOT_A = 0,
|
SLOT_A = 0,
|
||||||
SLOT_B = 1,
|
SLOT_B = 1,
|
||||||
GCI = 0,
|
|
||||||
SAV = 0x80,
|
|
||||||
GCS = 0x110,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class GCMemcardGetSaveDataRetVal
|
enum class GCMemcardGetSaveDataRetVal
|
||||||
@ -45,26 +42,10 @@ enum class GCMemcardGetSaveDataRetVal
|
|||||||
enum class GCMemcardImportFileRetVal
|
enum class GCMemcardImportFileRetVal
|
||||||
{
|
{
|
||||||
SUCCESS,
|
SUCCESS,
|
||||||
FAIL,
|
|
||||||
NOMEMCARD,
|
NOMEMCARD,
|
||||||
OUTOFDIRENTRIES,
|
OUTOFDIRENTRIES,
|
||||||
OUTOFBLOCKS,
|
OUTOFBLOCKS,
|
||||||
TITLEPRESENT,
|
TITLEPRESENT,
|
||||||
INVALIDFILESIZE,
|
|
||||||
GCSFAIL,
|
|
||||||
SAVFAIL,
|
|
||||||
OPENFAIL,
|
|
||||||
LENGTHFAIL,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class GCMemcardExportFileRetVal
|
|
||||||
{
|
|
||||||
SUCCESS,
|
|
||||||
FAIL,
|
|
||||||
NOMEMCARD,
|
|
||||||
OPENFAIL,
|
|
||||||
WRITEFAIL,
|
|
||||||
UNUSED,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class GCMemcardRemoveFileRetVal
|
enum class GCMemcardRemoveFileRetVal
|
||||||
@ -400,6 +381,12 @@ static_assert(sizeof(BlockAlloc) == BLOCK_SIZE);
|
|||||||
static_assert(std::is_trivially_copyable_v<BlockAlloc>);
|
static_assert(std::is_trivially_copyable_v<BlockAlloc>);
|
||||||
#pragma pack(pop)
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
struct Savefile
|
||||||
|
{
|
||||||
|
DEntry dir_entry;
|
||||||
|
std::vector<GCMBlock> blocks;
|
||||||
|
};
|
||||||
|
|
||||||
class GCMemcard
|
class GCMemcard
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
@ -419,8 +406,6 @@ private:
|
|||||||
|
|
||||||
GCMemcard();
|
GCMemcard();
|
||||||
|
|
||||||
GCMemcardImportFileRetVal ImportGciInternal(File::IOFile&& gci, const std::string& inputFile);
|
|
||||||
|
|
||||||
const Directory& GetActiveDirectory() const;
|
const Directory& GetActiveDirectory() const;
|
||||||
const BlockAlloc& GetActiveBat() const;
|
const BlockAlloc& GetActiveBat() const;
|
||||||
|
|
||||||
@ -496,26 +481,15 @@ public:
|
|||||||
|
|
||||||
GCMemcardGetSaveDataRetVal GetSaveData(u8 index, std::vector<GCMBlock>& saveBlocks) const;
|
GCMemcardGetSaveDataRetVal GetSaveData(u8 index, std::vector<GCMBlock>& saveBlocks) const;
|
||||||
|
|
||||||
// adds the file to the directory and copies its contents
|
// Adds the given savefile to the memory card, if possible.
|
||||||
GCMemcardImportFileRetVal ImportFile(const DEntry& direntry, std::vector<GCMBlock>& saveBlocks);
|
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
|
// delete a file from the directory
|
||||||
GCMemcardRemoveFileRetVal RemoveFile(u8 index);
|
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
|
// reads the banner image
|
||||||
std::optional<std::vector<u32>> ReadBannerRGBA8(u8 index) const;
|
std::optional<std::vector<u32>> ReadBannerRGBA8(u8 index) const;
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
#include "Common/Assert.h"
|
#include "Common/Assert.h"
|
||||||
#include "Common/ChunkFile.h"
|
#include "Common/ChunkFile.h"
|
||||||
|
#include "Common/CommonPaths.h"
|
||||||
#include "Common/CommonTypes.h"
|
#include "Common/CommonTypes.h"
|
||||||
#include "Common/Config/Config.h"
|
#include "Common/Config/Config.h"
|
||||||
#include "Common/FileSearch.h"
|
#include "Common/FileSearch.h"
|
||||||
@ -32,6 +33,8 @@
|
|||||||
#include "Core/ConfigManager.h"
|
#include "Core/ConfigManager.h"
|
||||||
#include "Core/Core.h"
|
#include "Core/Core.h"
|
||||||
#include "Core/HW/EXI/EXI_DeviceIPL.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/HW/Sram.h"
|
||||||
#include "Core/NetPlayProto.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++)
|
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 "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"
|
#include "Core/HW/GCMemcard/GCMemcard.h"
|
||||||
|
|
||||||
namespace Memcard
|
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)
|
bool HasSameIdentity(const DEntry& lhs, const DEntry& rhs)
|
||||||
{
|
{
|
||||||
// The Gamecube BIOS identifies two files as being 'the same' (that is, disallows copying from one
|
// 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
|
// 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
|
// 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)
|
if (lhs.m_gamecode != rhs.m_gamecode)
|
||||||
return false;
|
return false;
|
||||||
@ -53,4 +71,289 @@ bool HasSameIdentity(const DEntry& lhs, const DEntry& rhs)
|
|||||||
|
|
||||||
return true;
|
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
|
} // namespace Memcard
|
||||||
|
@ -4,9 +4,48 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
#include "Core/HW/GCMemcard/GCMemcard.h"
|
||||||
|
|
||||||
namespace Memcard
|
namespace Memcard
|
||||||
{
|
{
|
||||||
struct DEntry;
|
|
||||||
|
|
||||||
bool HasSameIdentity(const DEntry& lhs, const DEntry& rhs);
|
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
|
} // namespace Memcard
|
||||||
|
@ -5,6 +5,11 @@
|
|||||||
#include "DolphinQt/GCMemcardManager.h"
|
#include "DolphinQt/GCMemcardManager.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fmt/format.h>
|
||||||
|
|
||||||
#include <QDialogButtonBox>
|
#include <QDialogButtonBox>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
@ -15,25 +20,41 @@
|
|||||||
#include <QImage>
|
#include <QImage>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
|
#include <QMenu>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QTableWidget>
|
#include <QTableWidget>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
#include <QToolButton>
|
||||||
|
|
||||||
|
#include "Common/CommonPaths.h"
|
||||||
#include "Common/Config/Config.h"
|
#include "Common/Config/Config.h"
|
||||||
#include "Common/FileUtil.h"
|
#include "Common/FileUtil.h"
|
||||||
#include "Common/MsgHandler.h"
|
#include "Common/MsgHandler.h"
|
||||||
#include "Common/StringUtil.h"
|
#include "Common/StringUtil.h"
|
||||||
|
#include "Common/VariantUtil.h"
|
||||||
|
|
||||||
#include "Core/Config/MainSettings.h"
|
#include "Core/Config/MainSettings.h"
|
||||||
#include "Core/HW/GCMemcard/GCMemcard.h"
|
#include "Core/HW/GCMemcard/GCMemcard.h"
|
||||||
|
#include "Core/HW/GCMemcard/GCMemcardUtils.h"
|
||||||
|
|
||||||
#include "DolphinQt/GCMemcardCreateNewDialog.h"
|
#include "DolphinQt/GCMemcardCreateNewDialog.h"
|
||||||
#include "DolphinQt/QtUtils/ModalMessageBox.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
|
struct GCMemcardManager::IconAnimationData
|
||||||
{
|
{
|
||||||
@ -78,11 +99,20 @@ void GCMemcardManager::CreateWidgets()
|
|||||||
// Actions
|
// Actions
|
||||||
m_select_button = new QPushButton;
|
m_select_button = new QPushButton;
|
||||||
m_copy_button = new QPushButton;
|
m_copy_button = new QPushButton;
|
||||||
|
|
||||||
// Contents will be set by their appropriate functions
|
|
||||||
m_delete_button = new QPushButton(tr("&Delete"));
|
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_import_button = new QPushButton(tr("&Import..."));
|
||||||
m_fix_checksums_button = new QPushButton(tr("Fix Checksums"));
|
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]->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||||
m_slot_table[i]->setSelectionBehavior(QAbstractItemView::SelectRows);
|
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()->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]->verticalHeader()->hide();
|
||||||
m_slot_table[i]->setShowGrid(false);
|
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_table[i], 1, 0, 1, 3);
|
||||||
slot_layout->addWidget(m_slot_stat_label[i], 2, 0);
|
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);
|
UpdateSlotTable(i);
|
||||||
}
|
}
|
||||||
@ -123,10 +170,9 @@ void GCMemcardManager::CreateWidgets()
|
|||||||
layout->addWidget(m_copy_button, 2, 1);
|
layout->addWidget(m_copy_button, 2, 1);
|
||||||
layout->addWidget(m_delete_button, 3, 1);
|
layout->addWidget(m_delete_button, 3, 1);
|
||||||
layout->addWidget(m_export_button, 4, 1);
|
layout->addWidget(m_export_button, 4, 1);
|
||||||
layout->addWidget(m_export_all_button, 5, 1);
|
layout->addWidget(m_import_button, 5, 1);
|
||||||
layout->addWidget(m_import_button, 6, 1);
|
layout->addWidget(m_fix_checksums_button, 6, 1);
|
||||||
layout->addWidget(m_fix_checksums_button, 7, 1);
|
layout->addWidget(m_button_box, 8, 2);
|
||||||
layout->addWidget(m_button_box, 9, 2);
|
|
||||||
|
|
||||||
setLayout(layout);
|
setLayout(layout);
|
||||||
}
|
}
|
||||||
@ -135,8 +181,12 @@ void GCMemcardManager::ConnectWidgets()
|
|||||||
{
|
{
|
||||||
connect(m_button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
connect(m_button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||||
connect(m_select_button, &QPushButton::clicked, [this] { SetActiveSlot(!m_active_slot); });
|
connect(m_select_button, &QPushButton::clicked, [this] { SetActiveSlot(!m_active_slot); });
|
||||||
connect(m_export_button, &QPushButton::clicked, [this] { ExportFiles(true); });
|
connect(m_export_gci_action, &QAction::triggered,
|
||||||
connect(m_export_all_button, &QPushButton::clicked, this, &GCMemcardManager::ExportAllFiles);
|
[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_delete_button, &QPushButton::clicked, this, &GCMemcardManager::DeleteFiles);
|
||||||
connect(m_import_button, &QPushButton::clicked, this, &GCMemcardManager::ImportFile);
|
connect(m_import_button, &QPushButton::clicked, this, &GCMemcardManager::ImportFile);
|
||||||
connect(m_copy_button, &QPushButton::clicked, this, &GCMemcardManager::CopyFiles);
|
connect(m_copy_button, &QPushButton::clicked, this, &GCMemcardManager::CopyFiles);
|
||||||
@ -188,67 +238,69 @@ void GCMemcardManager::SetActiveSlot(int slot)
|
|||||||
void GCMemcardManager::UpdateSlotTable(int slot)
|
void GCMemcardManager::UpdateSlotTable(int slot)
|
||||||
{
|
{
|
||||||
m_slot_active_icons[slot].clear();
|
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)
|
if (m_slot_memcard[slot] == nullptr)
|
||||||
|
{
|
||||||
|
m_slot_table[slot]->setRowCount(0);
|
||||||
|
m_slot_stat_label[slot]->clear();
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto& memcard = m_slot_memcard[slot];
|
auto& memcard = m_slot_memcard[slot];
|
||||||
auto* table = m_slot_table[slot];
|
auto* table = m_slot_table[slot];
|
||||||
|
table->setSortingEnabled(false);
|
||||||
const auto create_item = [](const QString& string = {}) {
|
|
||||||
QTableWidgetItem* item = new QTableWidgetItem(string);
|
|
||||||
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
|
|
||||||
return item;
|
|
||||||
};
|
|
||||||
|
|
||||||
const u8 num_files = memcard->GetNumFiles();
|
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++)
|
for (int i = 0; i < num_files; i++)
|
||||||
{
|
{
|
||||||
int file_index = memcard->GetFileIndex(i);
|
const u8 file_index = memcard->GetFileIndex(i);
|
||||||
table->setRowCount(i + 1);
|
|
||||||
|
|
||||||
const auto file_comments = memcard->GetSaveComments(file_index);
|
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;
|
const QString title =
|
||||||
QString comment;
|
file_comments ? QString::fromStdString(file_comments->first).trimmed() : QString();
|
||||||
if (file_comments)
|
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);
|
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
|
||||||
comment = QString::fromStdString(file_comments->second);
|
item->setData(Qt::UserRole, static_cast<int>(file_index));
|
||||||
}
|
}
|
||||||
|
|
||||||
QString blocks = QStringLiteral("%1").arg(memcard->DEntry_BlockCount(file_index));
|
m_slot_active_icons[slot].emplace(file_index, std::move(icon_data));
|
||||||
QString block_count = QStringLiteral("%1").arg(memcard->DEntry_FirstBlock(file_index));
|
|
||||||
|
|
||||||
auto* banner = new QTableWidgetItem;
|
table->setItem(i, COLUMN_INDEX_FILENAME, item_filename);
|
||||||
banner->setData(Qt::DecorationRole, GetBannerFromSaveFile(file_index, slot));
|
table->setItem(i, COLUMN_INDEX_BANNER, item_banner);
|
||||||
banner->setFlags(banner->flags() ^ Qt::ItemIsEditable);
|
table->setItem(i, COLUMN_INDEX_TEXT, item_text);
|
||||||
|
table->setItem(i, COLUMN_INDEX_ICON, item_icon);
|
||||||
auto icon_data = GetIconFromSaveFile(file_index, slot);
|
table->setItem(i, COLUMN_INDEX_BLOCKS, item_blocks);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m_slot_stat_label[slot]->setText(tr("%1 Free Blocks; %2 Free Dir Entries")
|
const QString free_blocks_string = tr("Free Blocks: %1").arg(free_blocks);
|
||||||
.arg(memcard->GetFreeBlocks())
|
const QString free_files_string = tr("Free Files: %1").arg(free_files);
|
||||||
.arg(Memcard::DIRLEN - memcard->GetNumFiles()));
|
m_slot_stat_label[slot]->setText(
|
||||||
|
QStringLiteral("%1 %2").arg(free_blocks_string, free_files_string));
|
||||||
|
|
||||||
|
table->setSortingEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GCMemcardManager::UpdateActions()
|
void GCMemcardManager::UpdateActions()
|
||||||
@ -260,7 +312,6 @@ void GCMemcardManager::UpdateActions()
|
|||||||
|
|
||||||
m_copy_button->setEnabled(have_selection && have_memcard_other);
|
m_copy_button->setEnabled(have_selection && have_memcard_other);
|
||||||
m_export_button->setEnabled(have_selection);
|
m_export_button->setEnabled(have_selection);
|
||||||
m_export_all_button->setEnabled(have_memcard);
|
|
||||||
m_import_button->setEnabled(have_memcard);
|
m_import_button->setEnabled(have_memcard);
|
||||||
m_delete_button->setEnabled(have_selection);
|
m_delete_button->setEnabled(have_selection);
|
||||||
m_fix_checksums_button->setEnabled(have_memcard);
|
m_fix_checksums_button->setEnabled(have_memcard);
|
||||||
@ -278,7 +329,7 @@ void GCMemcardManager::SetSlotFile(int slot, QString path)
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
m_slot_memcard[slot] = nullptr;
|
m_slot_memcard[slot] = nullptr;
|
||||||
ModalMessageBox::critical(
|
ModalMessageBox::warning(
|
||||||
this, tr("Error"),
|
this, tr("Error"),
|
||||||
tr("Failed opening memory card:\n%1").arg(GetErrorMessagesForErrorCode(error_code)));
|
tr("Failed opening memory card:\n%1").arg(GetErrorMessagesForErrorCode(error_code)));
|
||||||
}
|
}
|
||||||
@ -293,162 +344,322 @@ void GCMemcardManager::SetSlotFileInteractive(int slot)
|
|||||||
this,
|
this,
|
||||||
slot == 0 ? tr("Set memory card file for Slot A") : tr("Set memory card file for Slot B"),
|
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)),
|
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())
|
if (!path.isEmpty())
|
||||||
m_slot_file_edit[slot]->setText(path);
|
m_slot_file_edit[slot]->setText(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GCMemcardManager::ExportFiles(bool prompt)
|
std::vector<u8> GCMemcardManager::GetSelectedFileIndices()
|
||||||
{
|
{
|
||||||
auto selection = m_slot_table[m_active_slot]->selectedItems();
|
const auto selection = m_slot_table[m_active_slot]->selectedItems();
|
||||||
auto& memcard = m_slot_memcard[m_active_slot];
|
std::vector<bool> lookup(Memcard::DIRLEN);
|
||||||
|
for (const auto* item : selection)
|
||||||
auto count = selection.count() / m_slot_table[m_active_slot]->columnCount();
|
|
||||||
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
{
|
||||||
auto sel = selection[i * m_slot_table[m_active_slot]->columnCount()];
|
const int index = item->data(Qt::UserRole).toInt();
|
||||||
int file_index = memcard->GetFileIndex(m_slot_table[m_active_slot]->row(sel));
|
if (index < 0 || index >= static_cast<int>(Memcard::DIRLEN))
|
||||||
|
{
|
||||||
|
ModalMessageBox::warning(this, tr("Error"),
|
||||||
|
tr("Data inconsistency in GCMemcardManager, aborting action."));
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
lookup[index] = true;
|
||||||
|
}
|
||||||
|
|
||||||
std::string gci_filename;
|
std::vector<u8> selected_indices;
|
||||||
if (!memcard->GCI_FileName(file_index, gci_filename))
|
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;
|
return;
|
||||||
|
|
||||||
QString path;
|
const std::string filename = qfilename.toStdString();
|
||||||
if (prompt)
|
if (!Memcard::WriteSavefile(filename, savefiles[0], format))
|
||||||
{
|
{
|
||||||
path = QFileDialog::getSaveFileName(
|
File::Delete(filename);
|
||||||
this, tr("Export Save File"),
|
ModalMessageBox::warning(this, tr("Export Failed"), tr("Failed to write savefile to disk."));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This is obviously intended to check for success instead.
|
return;
|
||||||
const auto exportRetval = memcard->ExportGci(file_index, path.toStdString(), "");
|
}
|
||||||
if (exportRetval == Memcard::GCMemcardExportFileRetVal::UNUSED)
|
|
||||||
|
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.") :
|
if (failures > 0)
|
||||||
tr("Successfully exported the %1 save files.").arg(count);
|
{
|
||||||
ModalMessageBox::information(this, tr("Success"), text);
|
QString failure_string =
|
||||||
|
tr("Failed to export %n out of %1 save file(s).", "", static_cast<int>(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<int>(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<Memcard::Savefile>& savefiles)
|
||||||
{
|
{
|
||||||
// This is nothing but a thin wrapper around ExportFiles()
|
auto& card = m_slot_memcard[slot];
|
||||||
m_slot_table[m_active_slot]->selectAll();
|
if (!card)
|
||||||
ExportFiles(false);
|
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<int>(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<int>(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()
|
void GCMemcardManager::ImportFile()
|
||||||
{
|
{
|
||||||
QString path = QFileDialog::getOpenFileName(
|
auto& card = m_slot_memcard[m_active_slot];
|
||||||
this, tr("Import Save File"), QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)),
|
if (!card)
|
||||||
tr("Native GCI File (*.gci)") + QStringLiteral(";;") + tr("MadCatz Gameshark files(*.gcs)") +
|
|
||||||
QStringLiteral(";;") + tr("Datel MaxDrive/Pro files(*.sav)"));
|
|
||||||
|
|
||||||
if (path.isEmpty())
|
|
||||||
return;
|
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<Memcard::Savefile> 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_slot_memcard[m_active_slot]->Save())
|
ImportFiles(m_active_slot, savefiles);
|
||||||
PanicAlertFmtT("File write failed");
|
|
||||||
|
|
||||||
UpdateSlotTable(m_active_slot);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GCMemcardManager::CopyFiles()
|
void GCMemcardManager::CopyFiles()
|
||||||
{
|
{
|
||||||
auto selection = m_slot_table[m_active_slot]->selectedItems();
|
const auto& source_card = m_slot_memcard[m_active_slot];
|
||||||
auto& memcard = 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()];
|
ModalMessageBox::warning(this, tr("Copy Failed"),
|
||||||
int file_index = memcard->GetFileIndex(m_slot_table[m_active_slot]->row(sel));
|
tr("Failed to read selected savefile(s) from memory card."));
|
||||||
|
return;
|
||||||
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"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < SLOT_COUNT; i++)
|
ImportFiles(!m_active_slot, savefiles);
|
||||||
{
|
|
||||||
if (!m_slot_memcard[i]->Save())
|
|
||||||
PanicAlertFmtT("File write failed");
|
|
||||||
|
|
||||||
UpdateSlotTable(i);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GCMemcardManager::DeleteFiles()
|
void GCMemcardManager::DeleteFiles()
|
||||||
{
|
{
|
||||||
auto selection = m_slot_table[m_active_slot]->selectedItems();
|
auto& card = m_slot_memcard[m_active_slot];
|
||||||
auto& memcard = 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
|
const QString text = tr("Do you want to delete the %n selected save file(s)?", "",
|
||||||
if (count > 1)
|
static_cast<int>(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?") :
|
if (card->RemoveFile(index) != Memcard::GCMemcardRemoveFileRetVal::SUCCESS)
|
||||||
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<int> 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)
|
|
||||||
{
|
{
|
||||||
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");
|
ModalMessageBox::warning(this, tr("Remove Failed"),
|
||||||
}
|
tr("Failed to write modified memory card to disk."));
|
||||||
else
|
|
||||||
{
|
|
||||||
ModalMessageBox::information(this, tr("Success"), tr("Successfully deleted files."));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateSlotTable(m_active_slot);
|
UpdateSlotTable(m_active_slot);
|
||||||
@ -461,7 +672,10 @@ void GCMemcardManager::FixChecksums()
|
|||||||
memcard->FixChecksums();
|
memcard->FixChecksums();
|
||||||
|
|
||||||
if (!memcard->Save())
|
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)
|
void GCMemcardManager::CreateNewCard(int slot)
|
||||||
@ -473,22 +687,37 @@ void GCMemcardManager::CreateNewCard(int slot)
|
|||||||
|
|
||||||
void GCMemcardManager::DrawIcons()
|
void GCMemcardManager::DrawIcons()
|
||||||
{
|
{
|
||||||
const auto column = 3;
|
const int column = COLUMN_INDEX_ICON;
|
||||||
for (int slot = 0; slot < SLOT_COUNT; slot++)
|
for (int slot = 0; slot < SLOT_COUNT; slot++)
|
||||||
{
|
{
|
||||||
// skip loop if the table is empty
|
QTableWidget* table = m_slot_table[slot];
|
||||||
if (m_slot_table[slot]->rowCount() <= 0)
|
const int row_count = table->rowCount();
|
||||||
|
|
||||||
|
if (row_count <= 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
const auto viewport = m_slot_table[slot]->viewport();
|
const auto viewport = table->viewport();
|
||||||
u32 row = m_slot_table[slot]->indexAt(viewport->rect().topLeft()).row();
|
const int viewport_first_row = table->indexAt(viewport->rect().topLeft()).row();
|
||||||
const auto max_table_index = m_slot_table[slot]->indexAt(viewport->rect().bottomLeft());
|
if (viewport_first_row >= row_count)
|
||||||
const u32 max_row =
|
continue;
|
||||||
max_table_index.row() < 0 ? (m_slot_table[slot]->rowCount() - 1) : max_table_index.row();
|
|
||||||
|
|
||||||
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<u8>(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
|
// this icon doesn't have an animation
|
||||||
if (icon.m_frames.size() <= 1)
|
if (icon.m_frames.size() <= 1)
|
||||||
@ -502,11 +731,7 @@ void GCMemcardManager::DrawIcons()
|
|||||||
if (prev_frame == current_frame)
|
if (prev_frame == current_frame)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
auto* item = new QTableWidgetItem;
|
|
||||||
item->setData(Qt::DecorationRole, icon.m_frames[current_frame]);
|
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'});
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@ -17,17 +18,23 @@ namespace Memcard
|
|||||||
{
|
{
|
||||||
class GCMemcard;
|
class GCMemcard;
|
||||||
class GCMemcardErrorCode;
|
class GCMemcardErrorCode;
|
||||||
|
struct Savefile;
|
||||||
|
enum class ReadSavefileErrorCode;
|
||||||
|
enum class SavefileFormat;
|
||||||
} // namespace Memcard
|
} // namespace Memcard
|
||||||
|
|
||||||
|
class QAction;
|
||||||
class QDialogButtonBox;
|
class QDialogButtonBox;
|
||||||
class QGroupBox;
|
class QGroupBox;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
class QLineEdit;
|
class QLineEdit;
|
||||||
|
class QMenu;
|
||||||
class QPixmap;
|
class QPixmap;
|
||||||
class QPushButton;
|
class QPushButton;
|
||||||
class QString;
|
class QString;
|
||||||
class QTableWidget;
|
class QTableWidget;
|
||||||
class QTimer;
|
class QTimer;
|
||||||
|
class QToolButton;
|
||||||
|
|
||||||
class GCMemcardManager : public QDialog
|
class GCMemcardManager : public QDialog
|
||||||
{
|
{
|
||||||
@ -37,6 +44,7 @@ public:
|
|||||||
~GCMemcardManager();
|
~GCMemcardManager();
|
||||||
|
|
||||||
static QString GetErrorMessagesForErrorCode(const Memcard::GCMemcardErrorCode& code);
|
static QString GetErrorMessagesForErrorCode(const Memcard::GCMemcardErrorCode& code);
|
||||||
|
static QString GetErrorMessageForErrorCode(Memcard::ReadSavefileErrorCode code);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct IconAnimationData;
|
struct IconAnimationData;
|
||||||
@ -51,11 +59,14 @@ private:
|
|||||||
void SetSlotFileInteractive(int slot);
|
void SetSlotFileInteractive(int slot);
|
||||||
void SetActiveSlot(int slot);
|
void SetActiveSlot(int slot);
|
||||||
|
|
||||||
|
std::vector<u8> GetSelectedFileIndices();
|
||||||
|
|
||||||
|
void ImportFiles(int slot, const std::vector<Memcard::Savefile>& savefiles);
|
||||||
|
|
||||||
void CopyFiles();
|
void CopyFiles();
|
||||||
void ImportFile();
|
void ImportFile();
|
||||||
void DeleteFiles();
|
void DeleteFiles();
|
||||||
void ExportFiles(bool prompt);
|
void ExportFiles(Memcard::SavefileFormat format);
|
||||||
void ExportAllFiles();
|
|
||||||
void FixChecksums();
|
void FixChecksums();
|
||||||
void CreateNewCard(int slot);
|
void CreateNewCard(int slot);
|
||||||
void DrawIcons();
|
void DrawIcons();
|
||||||
@ -67,15 +78,18 @@ private:
|
|||||||
// Actions
|
// Actions
|
||||||
QPushButton* m_select_button;
|
QPushButton* m_select_button;
|
||||||
QPushButton* m_copy_button;
|
QPushButton* m_copy_button;
|
||||||
QPushButton* m_export_button;
|
QToolButton* m_export_button;
|
||||||
QPushButton* m_export_all_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_import_button;
|
||||||
QPushButton* m_delete_button;
|
QPushButton* m_delete_button;
|
||||||
QPushButton* m_fix_checksums_button;
|
QPushButton* m_fix_checksums_button;
|
||||||
|
|
||||||
// Slots
|
// Slots
|
||||||
static constexpr int SLOT_COUNT = 2;
|
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<std::unique_ptr<Memcard::GCMemcard>, SLOT_COUNT> m_slot_memcard;
|
||||||
std::array<QGroupBox*, SLOT_COUNT> m_slot_group;
|
std::array<QGroupBox*, SLOT_COUNT> m_slot_group;
|
||||||
std::array<QLineEdit*, SLOT_COUNT> m_slot_file_edit;
|
std::array<QLineEdit*, SLOT_COUNT> m_slot_file_edit;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user