From 645ce6658c6be46b504b765add95de195aba85a2 Mon Sep 17 00:00:00 2001
From: Albert Liu <45282415+ggrtk@users.noreply.github.com>
Date: Thu, 4 Mar 2021 15:21:40 -0800
Subject: [PATCH 1/7] Qt/MemoryCardEditor: Don't show blocks label when no card
is selected
---
src/duckstation-qt/memorycardeditordialog.ui | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
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
+
From 47e2cd382b378e8f794422d32c4e588576dbe865 Mon Sep 17 00:00:00 2001
From: Albert Liu <45282415+ggrtk@users.noreply.github.com>
Date: Thu, 4 Mar 2021 15:37:27 -0800
Subject: [PATCH 2/7] Qt/MemoryCardEditor: Increase default filename column
size
---
src/duckstation-qt/memorycardeditordialog.cpp | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/duckstation-qt/memorycardeditordialog.cpp b/src/duckstation-qt/memorycardeditordialog.cpp
index be2676ee4..f0ddb65d3 100644
--- a/src/duckstation-qt/memorycardeditordialog.cpp
+++ b/src/duckstation-qt/memorycardeditordialog.cpp
@@ -53,8 +53,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)
From f112222fae5424a6b203e81fe4b7358e60cdaf93 Mon Sep 17 00:00:00 2001
From: Albert Liu <45282415+ggrtk@users.noreply.github.com>
Date: Thu, 4 Mar 2021 16:18:20 -0800
Subject: [PATCH 3/7] Qt/MemoryCardEditor: Prevent duplicate filenames when
copying saves
---
src/duckstation-qt/memorycardeditordialog.cpp | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/src/duckstation-qt/memorycardeditordialog.cpp b/src/duckstation-qt/memorycardeditordialog.cpp
index f0ddb65d3..cbb529ddd 100644
--- a/src/duckstation-qt/memorycardeditordialog.cpp
+++ b/src/duckstation-qt/memorycardeditordialog.cpp
@@ -331,6 +331,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"),
From 3088138c3c6611865d0fa3ffa2db237c09afb970 Mon Sep 17 00:00:00 2001
From: Albert Liu <45282415+ggrtk@users.noreply.github.com>
Date: Thu, 4 Mar 2021 16:42:26 -0800
Subject: [PATCH 4/7] Qt/MemoryCardEditor: Remove duplicate browse
functionality
---
src/duckstation-qt/memorycardeditordialog.cpp | 20 +------------------
1 file changed, 1 insertion(+), 19 deletions(-)
diff --git a/src/duckstation-qt/memorycardeditordialog.cpp b/src/duckstation-qt/memorycardeditordialog.cpp
index cbb529ddd..4c8fe8fe7 100644
--- a/src/duckstation-qt/memorycardeditordialog.cpp
+++ b/src/duckstation-qt/memorycardeditordialog.cpp
@@ -92,7 +92,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,24 +110,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();
- }
+ QString filename = card->path_cb->itemData(index).toString();
if (filename.isEmpty())
return;
From 0218006e1b8dfacbc497df3bdcdb8774551ffda6 Mon Sep 17 00:00:00 2001
From: Albert Liu <45282415+ggrtk@users.noreply.github.com>
Date: Thu, 4 Mar 2021 16:55:44 -0800
Subject: [PATCH 5/7] Qt/MemoryCardEditor: Remove unused context help button
---
src/duckstation-qt/memorycardeditordialog.cpp | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/duckstation-qt/memorycardeditordialog.cpp b/src/duckstation-qt/memorycardeditordialog.cpp
index 4c8fe8fe7..155c0a68f 100644
--- a/src/duckstation-qt/memorycardeditordialog.cpp
+++ b/src/duckstation-qt/memorycardeditordialog.cpp
@@ -15,6 +15,8 @@ static constexpr char MEMORY_CARD_IMPORT_FILTER[] =
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;
From e939507f42833b53ddbfe0ad06c12f1b872d54a1 Mon Sep 17 00:00:00 2001
From: Albert Liu <45282415+ggrtk@users.noreply.github.com>
Date: Thu, 4 Mar 2021 17:51:17 -0800
Subject: [PATCH 6/7] Qt/MemoryCardEditor: Handle switching to empty path at
combobox index 0
---
src/duckstation-qt/memorycardeditordialog.cpp | 15 +++++++++------
1 file changed, 9 insertions(+), 6 deletions(-)
diff --git a/src/duckstation-qt/memorycardeditordialog.cpp b/src/duckstation-qt/memorycardeditordialog.cpp
index 155c0a68f..422be3721 100644
--- a/src/duckstation-qt/memorycardeditordialog.cpp
+++ b/src/duckstation-qt/memorycardeditordialog.cpp
@@ -112,12 +112,7 @@ void MemoryCardEditorDialog::populateComboBox(QComboBox* cb)
void MemoryCardEditorDialog::loadCardFromComboBox(Card* card, int index)
{
- QString 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()
@@ -166,6 +161,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()))
{
@@ -435,4 +436,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);
}
From 50a78489f24d76f13a6fd0e638f72c3aa52d02fa Mon Sep 17 00:00:00 2001
From: Albert Liu <45282415+ggrtk@users.noreply.github.com>
Date: Thu, 4 Mar 2021 20:50:20 -0800
Subject: [PATCH 7/7] MemoryCardImage: Implement save file importing and
exporting
---
src/core/memory_card_image.cpp | 93 ++++++++++++++++++-
src/core/memory_card_image.h | 9 +-
src/duckstation-qt/memorycardeditordialog.cpp | 46 +++++++++
src/duckstation-qt/memorycardeditordialog.h | 3 +
4 files changed, 146 insertions(+), 5 deletions(-)
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 422be3721..1db23d233 100644
--- a/src/duckstation-qt/memorycardeditordialog.cpp
+++ b/src/duckstation-qt/memorycardeditordialog.cpp
@@ -11,6 +11,7 @@ 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)
{
@@ -85,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)
@@ -377,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);
@@ -402,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();
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();