GCMemcardManager.cpp 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869
  1. // Copyright 2018 Dolphin Emulator Project
  2. // SPDX-License-Identifier: GPL-2.0-or-later
  3. #include "DolphinQt/GCMemcardManager.h"
  4. #include <algorithm>
  5. #include <string>
  6. #include <vector>
  7. #include <fmt/format.h>
  8. #include <QDialogButtonBox>
  9. #include <QDir>
  10. #include <QGridLayout>
  11. #include <QGroupBox>
  12. #include <QHeaderView>
  13. #include <QImage>
  14. #include <QLabel>
  15. #include <QLineEdit>
  16. #include <QMenu>
  17. #include <QPixmap>
  18. #include <QPushButton>
  19. #include <QString>
  20. #include <QStringList>
  21. #include <QTableWidget>
  22. #include <QTimer>
  23. #include <QToolButton>
  24. #include "Common/Assert.h"
  25. #include "Common/CommonPaths.h"
  26. #include "Common/Config/Config.h"
  27. #include "Common/FileUtil.h"
  28. #include "Common/MsgHandler.h"
  29. #include "Common/StringUtil.h"
  30. #include "Common/VariantUtil.h"
  31. #include "Core/Config/MainSettings.h"
  32. #include "Core/HW/GCMemcard/GCMemcard.h"
  33. #include "Core/HW/GCMemcard/GCMemcardUtils.h"
  34. #include "DolphinQt/GCMemcardCreateNewDialog.h"
  35. #include "DolphinQt/QtUtils/DolphinFileDialog.h"
  36. #include "DolphinQt/QtUtils/ModalMessageBox.h"
  37. #include "DolphinQt/QtUtils/NonDefaultQPushButton.h"
  38. using namespace ExpansionInterface;
  39. constexpr int ROW_HEIGHT = 36;
  40. constexpr int COLUMN_WIDTH_FILENAME = 100;
  41. constexpr int COLUMN_WIDTH_BANNER = Memcard::MEMORY_CARD_BANNER_WIDTH + 6;
  42. constexpr int COLUMN_WIDTH_TEXT = 160;
  43. constexpr int COLUMN_WIDTH_ICON = Memcard::MEMORY_CARD_ICON_WIDTH + 6;
  44. constexpr int COLUMN_WIDTH_BLOCKS = 40;
  45. constexpr int COLUMN_INDEX_FILENAME = 0;
  46. constexpr int COLUMN_INDEX_BANNER = 1;
  47. constexpr int COLUMN_INDEX_TEXT = 2;
  48. constexpr int COLUMN_INDEX_ICON = 3;
  49. constexpr int COLUMN_INDEX_BLOCKS = 4;
  50. constexpr int COLUMN_COUNT = 5;
  51. namespace
  52. {
  53. Slot OtherSlot(Slot slot)
  54. {
  55. return slot == Slot::A ? Slot::B : Slot::A;
  56. }
  57. } // namespace
  58. struct GCMemcardManager::IconAnimationData
  59. {
  60. // the individual frames
  61. std::vector<QPixmap> m_frames;
  62. // vector containing a list of frame indices that indicate, for each time unit,
  63. // the frame that should be displayed when at that time unit
  64. std::vector<u8> m_frame_timing;
  65. };
  66. GCMemcardManager::GCMemcardManager(QWidget* parent) : QDialog(parent)
  67. {
  68. CreateWidgets();
  69. ConnectWidgets();
  70. SetActiveSlot(Slot::A);
  71. UpdateActions();
  72. m_timer = new QTimer(this);
  73. connect(m_timer, &QTimer::timeout, this, &GCMemcardManager::DrawIcons);
  74. // individual frames of icon animations can stay on screen for 4, 8, or 12 frames at 60 FPS,
  75. // which means the fastest animation and common denominator is 15 FPS or 66 milliseconds per frame
  76. m_timer->start(1000 / 15);
  77. LoadDefaultMemcards();
  78. // Make the dimensions more reasonable on startup
  79. resize(650, 500);
  80. setWindowTitle(tr("GameCube Memory Card Manager"));
  81. }
  82. GCMemcardManager::~GCMemcardManager() = default;
  83. void GCMemcardManager::CreateWidgets()
  84. {
  85. m_button_box = new QDialogButtonBox(QDialogButtonBox::Close);
  86. // Actions
  87. m_select_button = new NonDefaultQPushButton;
  88. m_copy_button = new NonDefaultQPushButton;
  89. m_delete_button = new NonDefaultQPushButton(tr("&Delete"));
  90. m_export_button = new QToolButton(this);
  91. m_export_menu = new QMenu(m_export_button);
  92. m_export_gci_action = new QAction(tr("&Export as .gci..."), m_export_menu);
  93. m_export_gcs_action = new QAction(tr("Export as .&gcs..."), m_export_menu);
  94. m_export_sav_action = new QAction(tr("Export as .&sav..."), m_export_menu);
  95. m_export_menu->addAction(m_export_gci_action);
  96. m_export_menu->addAction(m_export_gcs_action);
  97. m_export_menu->addAction(m_export_sav_action);
  98. m_export_button->setDefaultAction(m_export_gci_action);
  99. m_export_button->setPopupMode(QToolButton::MenuButtonPopup);
  100. m_export_button->setMenu(m_export_menu);
  101. m_import_button = new NonDefaultQPushButton(tr("&Import..."));
  102. m_fix_checksums_button = new NonDefaultQPushButton(tr("Fix Checksums"));
  103. auto* layout = new QGridLayout;
  104. for (Slot slot : MEMCARD_SLOTS)
  105. {
  106. m_slot_group[slot] = new QGroupBox(slot == Slot::A ? tr("Slot A") : tr("Slot B"));
  107. m_slot_file_edit[slot] = new QLineEdit;
  108. m_slot_open_button[slot] = new NonDefaultQPushButton(tr("&Open..."));
  109. m_slot_create_button[slot] = new NonDefaultQPushButton(tr("&Create..."));
  110. m_slot_table[slot] = new QTableWidget;
  111. m_slot_table[slot]->setTabKeyNavigation(false);
  112. m_slot_stat_label[slot] = new QLabel;
  113. m_slot_table[slot]->setSelectionMode(QAbstractItemView::ExtendedSelection);
  114. m_slot_table[slot]->setSelectionBehavior(QAbstractItemView::SelectRows);
  115. m_slot_table[slot]->setSortingEnabled(true);
  116. m_slot_table[slot]->horizontalHeader()->setHighlightSections(false);
  117. m_slot_table[slot]->horizontalHeader()->setMinimumSectionSize(0);
  118. m_slot_table[slot]->horizontalHeader()->setSortIndicatorShown(true);
  119. m_slot_table[slot]->setColumnCount(COLUMN_COUNT);
  120. m_slot_table[slot]->setHorizontalHeaderItem(COLUMN_INDEX_FILENAME,
  121. new QTableWidgetItem(tr("Filename")));
  122. m_slot_table[slot]->setHorizontalHeaderItem(COLUMN_INDEX_BANNER,
  123. new QTableWidgetItem(tr("Banner")));
  124. m_slot_table[slot]->setHorizontalHeaderItem(COLUMN_INDEX_TEXT,
  125. new QTableWidgetItem(tr("Title")));
  126. m_slot_table[slot]->setHorizontalHeaderItem(COLUMN_INDEX_ICON,
  127. new QTableWidgetItem(tr("Icon")));
  128. m_slot_table[slot]->setHorizontalHeaderItem(COLUMN_INDEX_BLOCKS,
  129. new QTableWidgetItem(tr("Blocks")));
  130. m_slot_table[slot]->setColumnWidth(COLUMN_INDEX_FILENAME, COLUMN_WIDTH_FILENAME);
  131. m_slot_table[slot]->setColumnWidth(COLUMN_INDEX_BANNER, COLUMN_WIDTH_BANNER);
  132. m_slot_table[slot]->setColumnWidth(COLUMN_INDEX_TEXT, COLUMN_WIDTH_TEXT);
  133. m_slot_table[slot]->setColumnWidth(COLUMN_INDEX_ICON, COLUMN_WIDTH_ICON);
  134. m_slot_table[slot]->setColumnWidth(COLUMN_INDEX_BLOCKS, COLUMN_WIDTH_BLOCKS);
  135. m_slot_table[slot]->verticalHeader()->setDefaultSectionSize(ROW_HEIGHT);
  136. m_slot_table[slot]->verticalHeader()->hide();
  137. m_slot_table[slot]->setShowGrid(false);
  138. auto* slot_layout = new QGridLayout;
  139. m_slot_group[slot]->setLayout(slot_layout);
  140. slot_layout->addWidget(m_slot_file_edit[slot], 0, 0);
  141. slot_layout->addWidget(m_slot_open_button[slot], 0, 1);
  142. slot_layout->addWidget(m_slot_create_button[slot], 0, 2);
  143. slot_layout->addWidget(m_slot_table[slot], 1, 0, 1, 3);
  144. slot_layout->addWidget(m_slot_stat_label[slot], 2, 0);
  145. layout->addWidget(m_slot_group[slot], 0, slot == Slot::A ? 0 : 2, 8, 1);
  146. UpdateSlotTable(slot);
  147. }
  148. layout->addWidget(m_select_button, 1, 1);
  149. layout->addWidget(m_copy_button, 2, 1);
  150. layout->addWidget(m_delete_button, 3, 1);
  151. layout->addWidget(m_export_button, 4, 1);
  152. layout->addWidget(m_import_button, 5, 1);
  153. layout->addWidget(m_fix_checksums_button, 6, 1);
  154. layout->addWidget(m_button_box, 8, 2);
  155. setLayout(layout);
  156. }
  157. void GCMemcardManager::ConnectWidgets()
  158. {
  159. connect(m_button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
  160. connect(m_select_button, &QPushButton::clicked,
  161. [this] { SetActiveSlot(OtherSlot(m_active_slot)); });
  162. connect(m_export_gci_action, &QAction::triggered,
  163. [this] { ExportFiles(Memcard::SavefileFormat::GCI); });
  164. connect(m_export_gcs_action, &QAction::triggered,
  165. [this] { ExportFiles(Memcard::SavefileFormat::GCS); });
  166. connect(m_export_sav_action, &QAction::triggered,
  167. [this] { ExportFiles(Memcard::SavefileFormat::SAV); });
  168. connect(m_delete_button, &QPushButton::clicked, this, &GCMemcardManager::DeleteFiles);
  169. connect(m_import_button, &QPushButton::clicked, this, &GCMemcardManager::ImportFile);
  170. connect(m_copy_button, &QPushButton::clicked, this, &GCMemcardManager::CopyFiles);
  171. connect(m_fix_checksums_button, &QPushButton::clicked, this, &GCMemcardManager::FixChecksums);
  172. for (Slot slot : MEMCARD_SLOTS)
  173. {
  174. connect(m_slot_file_edit[slot], &QLineEdit::textChanged,
  175. [this, slot](const QString& path) { SetSlotFile(slot, path); });
  176. connect(m_slot_open_button[slot], &QPushButton::clicked,
  177. [this, slot] { SetSlotFileInteractive(slot); });
  178. connect(m_slot_create_button[slot], &QPushButton::clicked,
  179. [this, slot] { CreateNewCard(slot); });
  180. connect(m_slot_table[slot], &QTableWidget::itemSelectionChanged, this,
  181. &GCMemcardManager::UpdateActions);
  182. }
  183. }
  184. void GCMemcardManager::LoadDefaultMemcards()
  185. {
  186. for (ExpansionInterface::Slot slot : ExpansionInterface::MEMCARD_SLOTS)
  187. {
  188. if (Config::Get(Config::GetInfoForEXIDevice(slot)) !=
  189. ExpansionInterface::EXIDeviceType::MemoryCard)
  190. {
  191. continue;
  192. }
  193. const QString path = QString::fromStdString(
  194. Config::GetMemcardPath(slot, Config::Get(Config::MAIN_FALLBACK_REGION)));
  195. SetSlotFile(slot, path);
  196. }
  197. }
  198. void GCMemcardManager::SetActiveSlot(Slot slot)
  199. {
  200. for (Slot slot2 : MEMCARD_SLOTS)
  201. m_slot_table[slot2]->setEnabled(slot2 == slot);
  202. m_select_button->setText(slot == Slot::A ? tr("Switch to B") : tr("Switch to A"));
  203. m_copy_button->setText(slot == Slot::A ? tr("Copy to B") : tr("Copy to A"));
  204. m_active_slot = slot;
  205. UpdateSlotTable(slot);
  206. UpdateActions();
  207. }
  208. void GCMemcardManager::UpdateSlotTable(Slot slot)
  209. {
  210. m_slot_active_icons[slot].clear();
  211. if (m_slot_memcard[slot] == nullptr)
  212. {
  213. m_slot_table[slot]->setRowCount(0);
  214. m_slot_stat_label[slot]->clear();
  215. return;
  216. }
  217. auto& memcard = m_slot_memcard[slot];
  218. auto* table = m_slot_table[slot];
  219. table->setSortingEnabled(false);
  220. const u8 num_files = memcard->GetNumFiles();
  221. const u8 free_files = Memcard::DIRLEN - num_files;
  222. const u16 free_blocks = memcard->GetFreeBlocks();
  223. table->setRowCount(num_files);
  224. for (int i = 0; i < num_files; i++)
  225. {
  226. const u8 file_index = memcard->GetFileIndex(i);
  227. const auto file_comments = memcard->GetSaveComments(file_index);
  228. const u16 block_count = memcard->DEntry_BlockCount(file_index);
  229. const auto entry = memcard->GetDEntry(file_index);
  230. const std::string filename = entry ? Memcard::GenerateFilename(*entry) : "";
  231. const QString title =
  232. file_comments ? QString::fromStdString(file_comments->first).trimmed() : QString();
  233. const QString comment =
  234. file_comments ? QString::fromStdString(file_comments->second).trimmed() : QString();
  235. auto banner = GetBannerFromSaveFile(file_index, slot);
  236. auto icon_data = GetIconFromSaveFile(file_index, slot);
  237. auto* item_filename = new QTableWidgetItem(QString::fromStdString(filename));
  238. auto* item_banner = new QTableWidgetItem();
  239. auto* item_text = new QTableWidgetItem(QStringLiteral("%1\n%2").arg(title, comment));
  240. auto* item_icon = new QTableWidgetItem();
  241. auto* item_blocks = new QTableWidgetItem();
  242. item_banner->setData(Qt::DecorationRole, banner);
  243. item_icon->setData(Qt::DecorationRole, icon_data.m_frames[0]);
  244. item_blocks->setData(Qt::DisplayRole, block_count);
  245. for (auto* item : {item_filename, item_banner, item_text, item_icon, item_blocks})
  246. {
  247. item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
  248. item->setData(Qt::UserRole, static_cast<int>(file_index));
  249. }
  250. m_slot_active_icons[slot].emplace(file_index, std::move(icon_data));
  251. table->setItem(i, COLUMN_INDEX_FILENAME, item_filename);
  252. table->setItem(i, COLUMN_INDEX_BANNER, item_banner);
  253. table->setItem(i, COLUMN_INDEX_TEXT, item_text);
  254. table->setItem(i, COLUMN_INDEX_ICON, item_icon);
  255. table->setItem(i, COLUMN_INDEX_BLOCKS, item_blocks);
  256. }
  257. const QString free_blocks_string = tr("Free Blocks: %1").arg(free_blocks);
  258. const QString free_files_string = tr("Free Files: %1").arg(free_files);
  259. m_slot_stat_label[slot]->setText(
  260. QStringLiteral("%1 %2").arg(free_blocks_string, free_files_string));
  261. table->setSortingEnabled(true);
  262. }
  263. void GCMemcardManager::UpdateActions()
  264. {
  265. auto selection = m_slot_table[m_active_slot]->selectedItems();
  266. bool have_selection = selection.count();
  267. bool have_memcard = m_slot_memcard[m_active_slot] != nullptr;
  268. bool have_memcard_other = m_slot_memcard[OtherSlot(m_active_slot)] != nullptr;
  269. m_copy_button->setEnabled(have_selection && have_memcard_other);
  270. m_export_button->setEnabled(have_selection);
  271. m_import_button->setEnabled(have_memcard);
  272. m_delete_button->setEnabled(have_selection);
  273. m_fix_checksums_button->setEnabled(have_memcard);
  274. }
  275. void GCMemcardManager::SetSlotFile(Slot slot, QString path)
  276. {
  277. auto [error_code, memcard] = Memcard::GCMemcard::Open(path.toStdString());
  278. if (!error_code.HasCriticalErrors() && memcard && memcard->IsValid())
  279. {
  280. m_slot_file_edit[slot]->setText(path);
  281. m_slot_memcard[slot] = std::make_unique<Memcard::GCMemcard>(std::move(*memcard));
  282. }
  283. else
  284. {
  285. m_slot_memcard[slot] = nullptr;
  286. ModalMessageBox::warning(
  287. this, tr("Error"),
  288. tr("Failed opening memory card:\n%1").arg(GetErrorMessagesForErrorCode(error_code)));
  289. }
  290. UpdateSlotTable(slot);
  291. UpdateActions();
  292. }
  293. void GCMemcardManager::SetSlotFileInteractive(Slot slot)
  294. {
  295. QString path = QDir::toNativeSeparators(
  296. DolphinFileDialog::getOpenFileName(this,
  297. slot == Slot::A ? tr("Set Memory Card File for Slot A") :
  298. tr("Set Memory Card File for Slot B"),
  299. QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)),
  300. QStringLiteral("%1 (*.raw *.gcp);;%2 (*)")
  301. .arg(tr("GameCube Memory Cards"), tr("All Files"))));
  302. if (!path.isEmpty())
  303. m_slot_file_edit[slot]->setText(path);
  304. }
  305. std::vector<u8> GCMemcardManager::GetSelectedFileIndices()
  306. {
  307. const auto selection = m_slot_table[m_active_slot]->selectedItems();
  308. std::vector<bool> lookup(Memcard::DIRLEN);
  309. for (const auto* item : selection)
  310. {
  311. const int index = item->data(Qt::UserRole).toInt();
  312. if (index < 0 || index >= static_cast<int>(Memcard::DIRLEN))
  313. {
  314. ModalMessageBox::warning(this, tr("Error"),
  315. tr("Data inconsistency in GCMemcardManager, aborting action."));
  316. return {};
  317. }
  318. lookup[index] = true;
  319. }
  320. std::vector<u8> selected_indices;
  321. for (u8 i = 0; i < Memcard::DIRLEN; ++i)
  322. {
  323. if (lookup[i])
  324. selected_indices.push_back(i);
  325. }
  326. return selected_indices;
  327. }
  328. static QString GetFormatDescription(Memcard::SavefileFormat format)
  329. {
  330. switch (format)
  331. {
  332. case Memcard::SavefileFormat::GCI:
  333. return QObject::tr("Native GCI File");
  334. case Memcard::SavefileFormat::GCS:
  335. return QObject::tr("MadCatz Gameshark files");
  336. case Memcard::SavefileFormat::SAV:
  337. return QObject::tr("Datel MaxDrive/Pro files");
  338. default:
  339. ASSERT(false);
  340. return QObject::tr("Native GCI File");
  341. }
  342. }
  343. void GCMemcardManager::ExportFiles(Memcard::SavefileFormat format)
  344. {
  345. const auto& memcard = m_slot_memcard[m_active_slot];
  346. if (!memcard)
  347. return;
  348. const auto selected_indices = GetSelectedFileIndices();
  349. if (selected_indices.empty())
  350. return;
  351. const auto savefiles = Memcard::GetSavefiles(*memcard, selected_indices);
  352. if (savefiles.empty())
  353. {
  354. ModalMessageBox::warning(this, tr("Export Failed"),
  355. tr("Failed to read selected savefile(s) from memory card."));
  356. return;
  357. }
  358. std::string extension = Memcard::GetDefaultExtension(format);
  359. if (savefiles.size() == 1)
  360. {
  361. // when exporting a single save file, let user specify exact path
  362. const std::string basename = Memcard::GenerateFilename(savefiles[0].dir_entry);
  363. const QString qformatdesc = GetFormatDescription(format);
  364. const std::string default_path =
  365. fmt::format("{}/{}{}", File::GetUserPath(D_GCUSER_IDX), basename, extension);
  366. const QString qfilename = DolphinFileDialog::getSaveFileName(
  367. this, tr("Export Save File"), QString::fromStdString(default_path),
  368. QStringLiteral("%1 (*%2);;%3 (*)")
  369. .arg(qformatdesc, QString::fromStdString(extension), tr("All Files")));
  370. if (qfilename.isEmpty())
  371. return;
  372. const std::string filename = qfilename.toStdString();
  373. if (!Memcard::WriteSavefile(filename, savefiles[0], format))
  374. {
  375. File::Delete(filename);
  376. ModalMessageBox::warning(this, tr("Export Failed"), tr("Failed to write savefile to disk."));
  377. }
  378. return;
  379. }
  380. const QString qdirpath = DolphinFileDialog::getExistingDirectory(
  381. this, QObject::tr("Export Save Files"),
  382. QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)));
  383. if (qdirpath.isEmpty())
  384. return;
  385. const std::string dirpath = qdirpath.toStdString();
  386. size_t failures = 0;
  387. for (const auto& savefile : savefiles)
  388. {
  389. // find a free filename so we don't overwrite anything
  390. const std::string basepath = dirpath + DIR_SEP + Memcard::GenerateFilename(savefile.dir_entry);
  391. std::string filename = basepath + extension;
  392. if (File::Exists(filename))
  393. {
  394. size_t tmp = 0;
  395. std::string free_name;
  396. do
  397. {
  398. free_name = fmt::format("{}_{}{}", basepath, tmp, extension);
  399. ++tmp;
  400. } while (File::Exists(free_name));
  401. filename = free_name;
  402. }
  403. if (!Memcard::WriteSavefile(filename, savefile, format))
  404. {
  405. File::Delete(filename);
  406. ++failures;
  407. }
  408. }
  409. if (failures > 0)
  410. {
  411. QString failure_string =
  412. tr("Failed to export %n out of %1 save file(s).", "", static_cast<int>(failures))
  413. .arg(savefiles.size());
  414. if (failures == savefiles.size())
  415. {
  416. ModalMessageBox::warning(this, tr("Export Failed"), failure_string);
  417. }
  418. else
  419. {
  420. QString success_string = tr("Successfully exported %n out of %1 save file(s).", "",
  421. static_cast<int>(savefiles.size() - failures))
  422. .arg(savefiles.size());
  423. ModalMessageBox::warning(this, tr("Export Failed"),
  424. QStringLiteral("%1\n%2").arg(failure_string, success_string));
  425. }
  426. }
  427. }
  428. void GCMemcardManager::ImportFiles(Slot slot, std::span<const Memcard::Savefile> savefiles)
  429. {
  430. auto& card = m_slot_memcard[slot];
  431. if (!card)
  432. return;
  433. const size_t number_of_files = savefiles.size();
  434. const size_t number_of_blocks = Memcard::GetBlockCount(savefiles);
  435. const size_t free_files = Memcard::DIRLEN - card->GetNumFiles();
  436. const size_t free_blocks = card->GetFreeBlocks();
  437. QStringList error_messages;
  438. if (number_of_files > free_files)
  439. {
  440. error_messages.push_back(
  441. tr("Not enough free files on the target memory card. At least %n free file(s) required.",
  442. "", static_cast<int>(number_of_files)));
  443. }
  444. if (number_of_blocks > free_blocks)
  445. {
  446. error_messages.push_back(
  447. tr("Not enough free blocks on the target memory card. At least %n free block(s) required.",
  448. "", static_cast<int>(number_of_blocks)));
  449. }
  450. if (Memcard::HasDuplicateIdentity(savefiles))
  451. {
  452. error_messages.push_back(
  453. tr("At least two of the selected save files have the same internal filename."));
  454. }
  455. for (const Memcard::Savefile& savefile : savefiles)
  456. {
  457. if (card->TitlePresent(savefile.dir_entry))
  458. {
  459. const std::string filename = Memcard::GenerateFilename(savefile.dir_entry);
  460. error_messages.push_back(tr("The target memory card already contains a file \"%1\".")
  461. .arg(QString::fromStdString(filename)));
  462. }
  463. }
  464. if (!error_messages.empty())
  465. {
  466. ModalMessageBox::warning(this, tr("Import Failed"), error_messages.join(QLatin1Char('\n')));
  467. return;
  468. }
  469. for (const Memcard::Savefile& savefile : savefiles)
  470. {
  471. const auto result = card->ImportFile(savefile);
  472. // we've already checked everything that could realistically fail here, so this should only
  473. // happen if the memory card data is corrupted in some way
  474. if (result != Memcard::GCMemcardImportFileRetVal::SUCCESS)
  475. {
  476. const std::string filename = Memcard::GenerateFilename(savefile.dir_entry);
  477. ModalMessageBox::warning(
  478. this, tr("Import Failed"),
  479. tr("Failed to import \"%1\".").arg(QString::fromStdString(filename)));
  480. break;
  481. }
  482. }
  483. if (!card->Save())
  484. {
  485. ModalMessageBox::warning(this, tr("Import Failed"),
  486. tr("Failed to write modified memory card to disk."));
  487. }
  488. UpdateSlotTable(slot);
  489. }
  490. void GCMemcardManager::ImportFile()
  491. {
  492. auto& card = m_slot_memcard[m_active_slot];
  493. if (!card)
  494. return;
  495. const QStringList paths = DolphinFileDialog::getOpenFileNames(
  496. this, tr("Import Save File(s)"), QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)),
  497. QStringLiteral("%1 (*.gci *.gcs *.sav);;%2 (*.gci);;%3 (*.gcs);;%4 (*.sav);;%5 (*)")
  498. .arg(tr("Supported file formats"), GetFormatDescription(Memcard::SavefileFormat::GCI),
  499. GetFormatDescription(Memcard::SavefileFormat::GCS),
  500. GetFormatDescription(Memcard::SavefileFormat::SAV), tr("All Files")));
  501. if (paths.isEmpty())
  502. return;
  503. std::vector<Memcard::Savefile> savefiles;
  504. savefiles.reserve(paths.size());
  505. QStringList errors;
  506. for (const QString& path : paths)
  507. {
  508. auto read_result = Memcard::ReadSavefile(path.toStdString());
  509. std::visit(overloaded{
  510. [&](Memcard::Savefile savefile) { savefiles.emplace_back(std::move(savefile)); },
  511. [&](Memcard::ReadSavefileErrorCode error_code) {
  512. errors.push_back(
  513. tr("%1: %2").arg(path, GetErrorMessageForErrorCode(error_code)));
  514. },
  515. },
  516. std::move(read_result));
  517. }
  518. if (!errors.empty())
  519. {
  520. ModalMessageBox::warning(
  521. this, tr("Import Failed"),
  522. tr("Encountered the following errors while opening save files:\n%1\n\nAborting import.")
  523. .arg(errors.join(QStringLiteral("\n"))));
  524. return;
  525. }
  526. ImportFiles(m_active_slot, savefiles);
  527. }
  528. void GCMemcardManager::CopyFiles()
  529. {
  530. const auto& source_card = m_slot_memcard[m_active_slot];
  531. if (!source_card)
  532. return;
  533. auto& target_card = m_slot_memcard[OtherSlot(m_active_slot)];
  534. if (!target_card)
  535. return;
  536. const auto selected_indices = GetSelectedFileIndices();
  537. if (selected_indices.empty())
  538. return;
  539. const auto savefiles = Memcard::GetSavefiles(*source_card, selected_indices);
  540. if (savefiles.empty())
  541. {
  542. ModalMessageBox::warning(this, tr("Copy Failed"),
  543. tr("Failed to read selected savefile(s) from memory card."));
  544. return;
  545. }
  546. ImportFiles(OtherSlot(m_active_slot), savefiles);
  547. }
  548. void GCMemcardManager::DeleteFiles()
  549. {
  550. auto& card = m_slot_memcard[m_active_slot];
  551. if (!card)
  552. return;
  553. const auto selected_indices = GetSelectedFileIndices();
  554. if (selected_indices.empty())
  555. return;
  556. const QString text = tr("Do you want to delete the %n selected save file(s)?", "",
  557. static_cast<int>(selected_indices.size()));
  558. const auto response = ModalMessageBox::question(this, tr("Question"), text);
  559. if (response != QMessageBox::Yes)
  560. return;
  561. for (const u8 index : selected_indices)
  562. {
  563. if (card->RemoveFile(index) != Memcard::GCMemcardRemoveFileRetVal::SUCCESS)
  564. {
  565. ModalMessageBox::warning(this, tr("Remove Failed"), tr("Failed to remove file."));
  566. break;
  567. }
  568. }
  569. if (!card->Save())
  570. {
  571. ModalMessageBox::warning(this, tr("Remove Failed"),
  572. tr("Failed to write modified memory card to disk."));
  573. }
  574. UpdateSlotTable(m_active_slot);
  575. UpdateActions();
  576. }
  577. void GCMemcardManager::FixChecksums()
  578. {
  579. auto& memcard = m_slot_memcard[m_active_slot];
  580. memcard->FixChecksums();
  581. if (!memcard->Save())
  582. {
  583. ModalMessageBox::warning(this, tr("Fix Checksums Failed"),
  584. tr("Failed to write modified memory card to disk."));
  585. }
  586. }
  587. void GCMemcardManager::CreateNewCard(Slot slot)
  588. {
  589. GCMemcardCreateNewDialog dialog(this);
  590. if (dialog.exec() == QDialog::Accepted)
  591. m_slot_file_edit[slot]->setText(QString::fromStdString(dialog.GetMemoryCardPath()));
  592. }
  593. void GCMemcardManager::DrawIcons()
  594. {
  595. const int column = COLUMN_INDEX_ICON;
  596. for (Slot slot : MEMCARD_SLOTS)
  597. {
  598. QTableWidget* table = m_slot_table[slot];
  599. const int row_count = table->rowCount();
  600. if (row_count <= 0)
  601. continue;
  602. const auto viewport = table->viewport();
  603. const int viewport_first_row = table->indexAt(viewport->rect().topLeft()).row();
  604. if (viewport_first_row >= row_count)
  605. continue;
  606. const int first_row = viewport_first_row < 0 ? 0 : viewport_first_row;
  607. const int viewport_last_row = table->indexAt(viewport->rect().bottomLeft()).row();
  608. const int last_row =
  609. viewport_last_row < 0 ? (row_count - 1) : std::min(viewport_last_row, row_count - 1);
  610. for (int row = first_row; row <= last_row; ++row)
  611. {
  612. auto* item = table->item(row, column);
  613. if (!item)
  614. continue;
  615. const u8 index = static_cast<u8>(item->data(Qt::UserRole).toInt());
  616. auto it = m_slot_active_icons[slot].find(index);
  617. if (it == m_slot_active_icons[slot].end())
  618. continue;
  619. const auto& icon = it->second;
  620. // this icon doesn't have an animation
  621. if (icon.m_frames.size() <= 1)
  622. continue;
  623. const u64 prev_time_in_animation = (m_current_frame - 1) % icon.m_frame_timing.size();
  624. const u8 prev_frame = icon.m_frame_timing[prev_time_in_animation];
  625. const u64 current_time_in_animation = m_current_frame % icon.m_frame_timing.size();
  626. const u8 current_frame = icon.m_frame_timing[current_time_in_animation];
  627. if (prev_frame == current_frame)
  628. continue;
  629. item->setData(Qt::DecorationRole, icon.m_frames[current_frame]);
  630. }
  631. }
  632. ++m_current_frame;
  633. }
  634. QPixmap GCMemcardManager::GetBannerFromSaveFile(int file_index, Slot slot)
  635. {
  636. auto& memcard = m_slot_memcard[slot];
  637. auto pxdata = memcard->ReadBannerRGBA8(file_index);
  638. QImage image;
  639. if (pxdata)
  640. {
  641. image = QImage(reinterpret_cast<u8*>(pxdata->data()), Memcard::MEMORY_CARD_BANNER_WIDTH,
  642. Memcard::MEMORY_CARD_BANNER_HEIGHT, QImage::Format_ARGB32);
  643. }
  644. return QPixmap::fromImage(image);
  645. }
  646. GCMemcardManager::IconAnimationData GCMemcardManager::GetIconFromSaveFile(int file_index, Slot slot)
  647. {
  648. auto& memcard = m_slot_memcard[slot];
  649. IconAnimationData frame_data;
  650. const auto decoded_data = memcard->ReadAnimRGBA8(file_index);
  651. // Decode Save File Animation
  652. if (decoded_data && !decoded_data->empty())
  653. {
  654. frame_data.m_frames.reserve(decoded_data->size());
  655. for (size_t f = 0; f < decoded_data->size(); ++f)
  656. {
  657. QImage img(reinterpret_cast<const u8*>((*decoded_data)[f].image_data.data()),
  658. Memcard::MEMORY_CARD_ICON_WIDTH, Memcard::MEMORY_CARD_ICON_HEIGHT,
  659. QImage::Format_ARGB32);
  660. frame_data.m_frames.push_back(QPixmap::fromImage(img));
  661. for (int i = 0; i < (*decoded_data)[f].delay; ++i)
  662. {
  663. frame_data.m_frame_timing.push_back(static_cast<u8>(f));
  664. }
  665. }
  666. const bool is_pingpong = memcard->DEntry_IsPingPong(file_index);
  667. if (is_pingpong && decoded_data->size() >= 3)
  668. {
  669. // if the animation 'ping-pongs' between start and end then the animation frame order is
  670. // something like 'abcdcbabcdcba' instead of the usual 'abcdabcdabcd'
  671. // to display that correctly just append all except the first and last frame in reverse order
  672. // at the end of the animation
  673. for (size_t f = decoded_data->size() - 2; f > 0; --f)
  674. {
  675. for (int i = 0; i < (*decoded_data)[f].delay; ++i)
  676. {
  677. frame_data.m_frame_timing.push_back(static_cast<u8>(f));
  678. }
  679. }
  680. }
  681. }
  682. else
  683. {
  684. // No Animation found, use an empty placeholder instead.
  685. frame_data.m_frames.emplace_back();
  686. frame_data.m_frame_timing.push_back(0);
  687. }
  688. return frame_data;
  689. }
  690. QString GCMemcardManager::GetErrorMessagesForErrorCode(const Memcard::GCMemcardErrorCode& code)
  691. {
  692. QStringList sl;
  693. if (code.Test(Memcard::GCMemcardValidityIssues::FAILED_TO_OPEN))
  694. sl.push_back(tr("Couldn't open file."));
  695. if (code.Test(Memcard::GCMemcardValidityIssues::IO_ERROR))
  696. sl.push_back(tr("Couldn't read file."));
  697. if (code.Test(Memcard::GCMemcardValidityIssues::INVALID_CARD_SIZE))
  698. sl.push_back(tr("Filesize does not match any known GameCube Memory Card size."));
  699. if (code.Test(Memcard::GCMemcardValidityIssues::MISMATCHED_CARD_SIZE))
  700. sl.push_back(tr("Filesize in header mismatches actual card size."));
  701. if (code.Test(Memcard::GCMemcardValidityIssues::INVALID_CHECKSUM))
  702. sl.push_back(tr("Invalid checksums."));
  703. if (code.Test(Memcard::GCMemcardValidityIssues::FREE_BLOCK_MISMATCH))
  704. sl.push_back(tr("Mismatch between free block count in header and actually unused blocks."));
  705. if (code.Test(Memcard::GCMemcardValidityIssues::DIR_BAT_INCONSISTENT))
  706. sl.push_back(tr("Mismatch between internal data structures."));
  707. if (code.Test(Memcard::GCMemcardValidityIssues::DATA_IN_UNUSED_AREA))
  708. sl.push_back(tr("Data in area of file that should be unused."));
  709. if (sl.empty())
  710. return tr("No errors.");
  711. return sl.join(QLatin1Char{'\n'});
  712. }
  713. QString GCMemcardManager::GetErrorMessageForErrorCode(Memcard::ReadSavefileErrorCode code)
  714. {
  715. switch (code)
  716. {
  717. case Memcard::ReadSavefileErrorCode::OpenFileFail:
  718. return tr("Failed to open file.");
  719. case Memcard::ReadSavefileErrorCode::IOError:
  720. return tr("Failed to read from file.");
  721. case Memcard::ReadSavefileErrorCode::DataCorrupted:
  722. return tr("Data in unrecognized format or corrupted.");
  723. default:
  724. return tr("Unknown error.");
  725. }
  726. }