diff --git a/src/core/memory_card_image.cpp b/src/core/memory_card_image.cpp index cebe37903..2e38d99c5 100644 --- a/src/core/memory_card_image.cpp +++ b/src/core/memory_card_image.cpp @@ -466,4 +466,95 @@ bool ImportCard(DataArray* data, const char* filename) } } -} // namespace MemoryCardImage \ No newline at end of file +bool ExportSave(DataArray* data, const FileInfo& fi, const char* filename) +{ + std::unique_ptr stream = + FileSystem::OpenFile(filename, BYTESTREAM_OPEN_CREATE | BYTESTREAM_OPEN_TRUNCATE | BYTESTREAM_OPEN_WRITE | + BYTESTREAM_OPEN_ATOMIC_UPDATE | BYTESTREAM_OPEN_STREAMED); + if (!stream) + { + Log_ErrorPrintf("Failed to open '%s' for writing.", filename); + return false; + } + + DirectoryFrame* df_ptr = GetFramePtr(data, 0, fi.first_block); + std::vector header = std::vector(static_cast(FRAME_SIZE)); + std::memcpy(header.data(), df_ptr, sizeof(*df_ptr)); + + std::vector blocks; + if (!ReadFile(*data, fi, &blocks)) + { + Log_ErrorPrintf("Failed to read save blocks from memory card data"); + return false; + } + + if (!stream->Write(header.data(), static_cast(header.size())) || + !stream->Write(blocks.data(), static_cast(blocks.size())) || !stream->Commit()) + { + Log_ErrorPrintf("Failed to write exported save to '%s'", filename); + stream->Discard(); + return false; + } + + return true; +} + +bool ImportSave(DataArray* data, const char* filename) +{ + FILESYSTEM_STAT_DATA sd; + if (!FileSystem::StatFile(filename, &sd)) + { + Log_ErrorPrintf("Failed to stat file '%s'", filename); + return false; + } + + // Make sure the size of the actual file is valid + if (sd.Size <= FRAME_SIZE || (sd.Size - FRAME_SIZE) % BLOCK_SIZE != 0u || (sd.Size - FRAME_SIZE) / BLOCK_SIZE > 15u) + { + Log_ErrorPrintf("Invalid size for save file '%s'", filename); + return false; + } + + std::unique_ptr stream = FileSystem::OpenFile(filename, BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_STREAMED); + if (!stream) + { + Log_ErrorPrintf("Failed to open '%s' for reading", filename); + return false; + } + + DirectoryFrame df; + if (stream->Read(&df, FRAME_SIZE) != FRAME_SIZE) + { + Log_ErrorPrintf("Failed to read directory frame from '%s'", filename); + return false; + } + + // Make sure the size reported by the directory frame is valid + if (df.file_size < BLOCK_SIZE || df.file_size % BLOCK_SIZE != 0 || df.file_size / BLOCK_SIZE > 15u) + { + Log_ErrorPrintf("Invalid size (%u bytes) reported by directory frame", df.file_size); + return false; + } + + // Make sure there isn't already a save with the same name + std::vector fileinfos = EnumerateFiles(*data); + for (const FileInfo& fi : fileinfos) + { + if (fi.filename.compare(0, sizeof(df.filename), df.filename) == 0) + { + Log_ErrorPrintf("Save file with the same name already exists in memory card"); + return false; + } + } + + std::vector blocks = std::vector(static_cast(df.file_size)); + if (stream->Read(blocks.data(), df.file_size) != df.file_size) + { + Log_ErrorPrintf("Failed to read block bytes from '%s'", filename); + return false; + } + + return WriteFile(data, df.filename, blocks); +} + +} // namespace MemoryCardImage diff --git a/src/core/memory_card_image.h b/src/core/memory_card_image.h index a45151106..9ade380cd 100644 --- a/src/core/memory_card_image.h +++ b/src/core/memory_card_image.h @@ -7,8 +7,7 @@ #include #include -namespace MemoryCardImage -{ +namespace MemoryCardImage { enum : u32 { DATA_SIZE = 128 * 1024, // 1mbit @@ -17,7 +16,7 @@ enum : u32 FRAMES_PER_BLOCK = BLOCK_SIZE / FRAME_SIZE, NUM_BLOCKS = DATA_SIZE / BLOCK_SIZE, NUM_FRAMES = DATA_SIZE / FRAME_SIZE, - ICON_WIDTH =16, + ICON_WIDTH = 16, ICON_HEIGHT = 16 }; @@ -50,4 +49,6 @@ bool ReadFile(const DataArray& data, const FileInfo& fi, std::vector* buffer bool WriteFile(DataArray* data, const std::string_view& filename, const std::vector& buffer); bool DeleteFile(DataArray* data, const FileInfo& fi); bool ImportCard(DataArray* data, const char* filename); -} +bool ExportSave(DataArray* data, const FileInfo& fi, const char* filename); +bool ImportSave(DataArray* data, const char* filename); +} // namespace MemoryCardImage diff --git a/src/duckstation-qt/memorycardeditordialog.cpp b/src/duckstation-qt/memorycardeditordialog.cpp index be2676ee4..1db23d233 100644 --- a/src/duckstation-qt/memorycardeditordialog.cpp +++ b/src/duckstation-qt/memorycardeditordialog.cpp @@ -11,10 +11,13 @@ static constexpr char MEMORY_CARD_IMAGE_FILTER[] = QT_TRANSLATE_NOOP("MemoryCardEditorDialog", "All Memory Card Types (*.mcd *.mcr *.mc)"); static constexpr char MEMORY_CARD_IMPORT_FILTER[] = QT_TRANSLATE_NOOP("MemoryCardEditorDialog", "All Importable Memory Card Types (*.mcd *.mcr *.mc *.gme)"); +static constexpr char SINGLE_SAVEFILE_FILTER[] = TRANSLATABLE("MemoryCardEditorDialog", "Single Save Files (*.mcs)"); MemoryCardEditorDialog::MemoryCardEditorDialog(QWidget* parent) : QDialog(parent) { m_ui.setupUi(this); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + m_card_a.path_cb = m_ui.cardAPath; m_card_a.table = m_ui.cardA; m_card_a.blocks_free_label = m_ui.cardAUsage; @@ -53,8 +56,8 @@ bool MemoryCardEditorDialog::setCardB(const QString& path) void MemoryCardEditorDialog::resizeEvent(QResizeEvent* ev) { - QtUtils::ResizeColumnsForTableView(m_card_a.table, {32, -1, 100, 45}); - QtUtils::ResizeColumnsForTableView(m_card_b.table, {32, -1, 100, 45}); + QtUtils::ResizeColumnsForTableView(m_card_a.table, {32, -1, 155, 45}); + QtUtils::ResizeColumnsForTableView(m_card_b.table, {32, -1, 155, 45}); } void MemoryCardEditorDialog::closeEvent(QCloseEvent* ev) @@ -83,6 +86,9 @@ void MemoryCardEditorDialog::connectUi() connect(m_ui.saveCardB, &QPushButton::clicked, [this]() { saveCard(&m_card_b); }); connect(m_ui.importCardA, &QPushButton::clicked, [this]() { importCard(&m_card_a); }); connect(m_ui.importCardB, &QPushButton::clicked, [this]() { importCard(&m_card_b); }); + connect(m_ui.exportFile, &QPushButton::clicked, this, &MemoryCardEditorDialog::doExportSaveFile); + connect(m_ui.importFileToCardA, &QPushButton::clicked, [this]() { importSaveFile(&m_card_a); }); + connect(m_ui.importFileToCardB, &QPushButton::clicked, [this]() { importSaveFile(&m_card_b); }); } void MemoryCardEditorDialog::populateComboBox(QComboBox* cb) @@ -92,7 +98,6 @@ void MemoryCardEditorDialog::populateComboBox(QComboBox* cb) cb->clear(); cb->addItem(QString()); - cb->addItem(tr("Browse...")); const std::string base_path(g_host_interface->GetUserDirectoryRelativePath("memcards")); FileSystem::FindResultsArray results; @@ -111,29 +116,7 @@ void MemoryCardEditorDialog::populateComboBox(QComboBox* cb) void MemoryCardEditorDialog::loadCardFromComboBox(Card* card, int index) { - QString filename; - if (index == 1) - { - filename = QDir::toNativeSeparators( - QFileDialog::getOpenFileName(this, tr("Select Memory Card"), QString(), tr(MEMORY_CARD_IMAGE_FILTER))); - if (!filename.isEmpty()) - { - // add to combo box - QFileInfo file(filename); - QSignalBlocker sb(card->path_cb); - card->path_cb->addItem(file.baseName(), QVariant(filename)); - card->path_cb->setCurrentIndex(card->path_cb->count() - 1); - } - } - else - { - filename = card->path_cb->itemData(index).toString(); - } - - if (filename.isEmpty()) - return; - - loadCard(filename, card); + loadCard(card->path_cb->itemData(index).toString(), card); } void MemoryCardEditorDialog::onCardASelectionChanged() @@ -182,6 +165,12 @@ bool MemoryCardEditorDialog::loadCard(const QString& filename, Card* card) card->filename.clear(); + if (filename.isEmpty()) + { + updateButtonState(); + return false; + } + std::string filename_str = filename.toStdString(); if (!MemoryCardImage::LoadFromFile(&card->data, filename_str.c_str())) { @@ -331,6 +320,19 @@ void MemoryCardEditorDialog::doCopyFile() Card* dst = (src == &m_card_a) ? &m_card_b : &m_card_a; + for (const MemoryCardImage::FileInfo& dst_fi : dst->files) + { + if (dst_fi.filename == fi->filename) + { + QMessageBox::critical( + this, tr("Error"), + tr("Destination memory card already contains a save file with the same name (%1) as the one you are attempting " + "to copy. Please delete this file from the destination memory card before copying.") + .arg(QString(fi->filename.c_str()))); + return; + } + } + if (dst->blocks_free < fi->num_blocks) { QMessageBox::critical(this, tr("Error"), @@ -379,6 +381,27 @@ void MemoryCardEditorDialog::doDeleteFile() updateButtonState(); } +void MemoryCardEditorDialog::doExportSaveFile() +{ + QString filename = QDir::toNativeSeparators( + QFileDialog::getSaveFileName(this, tr("Select Single Savefile"), QString(), tr(SINGLE_SAVEFILE_FILTER))); + + if (filename.isEmpty()) + return; + + const auto [card, fi] = getSelectedFile(); + if (!fi) + return; + + if (!MemoryCardImage::ExportSave(&card->data, *fi, filename.toStdString().c_str())) + { + QMessageBox::critical( + this, tr("Error"), + tr("Failed to export save file %1. Check the log for more details.").arg(QString::fromStdString(fi->filename))); + return; + } +} + void MemoryCardEditorDialog::importCard(Card* card) { promptForSave(card); @@ -404,6 +427,27 @@ void MemoryCardEditorDialog::importCard(Card* card) updateButtonState(); } +void MemoryCardEditorDialog::importSaveFile(Card* card) +{ + QString filename = + QFileDialog::getOpenFileName(this, tr("Select Import Save File"), QString(), tr(SINGLE_SAVEFILE_FILTER)); + + if (filename.isEmpty()) + return; + + if (!MemoryCardImage::ImportSave(&card->data, filename.toStdString().c_str())) + { + QMessageBox::critical(this, tr("Error"), + tr("Failed to import save. Check if there is enough room on the memory card or if an " + "existing save with the same name already exists.")); + return; + } + + updateCardTable(card); + updateCardBlocksFree(card); + setCardDirty(card); +} + std::tuple MemoryCardEditorDialog::getSelectedFile() { QList sel = m_card_a.table->selectedRanges(); @@ -438,4 +482,6 @@ void MemoryCardEditorDialog::updateButtonState() m_ui.moveRight->setEnabled(both_cards_present && has_selection && !is_card_b); m_ui.importCardA->setEnabled(card_a_present); m_ui.importCardB->setEnabled(card_b_present); + m_ui.importFileToCardA->setEnabled(card_a_present); + m_ui.importFileToCardB->setEnabled(card_b_present); } diff --git a/src/duckstation-qt/memorycardeditordialog.h b/src/duckstation-qt/memorycardeditordialog.h index d2b239f2c..d6d3cb35f 100644 --- a/src/duckstation-qt/memorycardeditordialog.h +++ b/src/duckstation-qt/memorycardeditordialog.h @@ -57,6 +57,9 @@ private: void promptForSave(Card* card); void importCard(Card* card); + void doExportSaveFile(); + void importSaveFile(Card* card); + std::tuple getSelectedFile(); void updateButtonState(); diff --git a/src/duckstation-qt/memorycardeditordialog.ui b/src/duckstation-qt/memorycardeditordialog.ui index c79527c81..9997651c7 100644 --- a/src/duckstation-qt/memorycardeditordialog.ui +++ b/src/duckstation-qt/memorycardeditordialog.ui @@ -6,7 +6,7 @@ 0 0 - 846 + 889 515 @@ -101,7 +101,7 @@ - 0 blocks used + @@ -142,7 +142,7 @@ - 0 blocks used +