Merge pull request #6327 from spycrab/qt_memcard_manager

Qt: Implement GameCube memcard manager
This commit is contained in:
Anthony 2018-01-31 13:22:40 -08:00 committed by GitHub
commit 1264daae9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 569 additions and 0 deletions

View File

@ -85,6 +85,7 @@ set(SRCS
GameList/GameTracker.cpp GameList/GameTracker.cpp
GameList/GridProxyModel.cpp GameList/GridProxyModel.cpp
GameList/ListProxyModel.cpp GameList/ListProxyModel.cpp
GCMemcardManager.cpp
QtUtils/BlockUserInputFilter.cpp QtUtils/BlockUserInputFilter.cpp
NetPlay/GameListDialog.cpp NetPlay/GameListDialog.cpp
NetPlay/MD5Dialog.cpp NetPlay/MD5Dialog.cpp

View File

@ -221,6 +221,7 @@
<ClCompile Include="Debugger\RegisterColumn.cpp" /> <ClCompile Include="Debugger\RegisterColumn.cpp" />
<ClCompile Include="Debugger\RegisterWidget.cpp" /> <ClCompile Include="Debugger\RegisterWidget.cpp" />
<ClCompile Include="Debugger\WatchWidget.cpp" /> <ClCompile Include="Debugger\WatchWidget.cpp" />
<ClCompile Include="GCMemcardManager.cpp" />
<ClCompile Include="GameList\GameFile.cpp" /> <ClCompile Include="GameList\GameFile.cpp" />
<ClCompile Include="GameList\GameFileCache.cpp" /> <ClCompile Include="GameList\GameFileCache.cpp" />
<ClCompile Include="GameList\GameList.cpp" /> <ClCompile Include="GameList\GameList.cpp" />

View File

@ -0,0 +1,476 @@
// Copyright 2018 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#include "DolphinQt2/GCMemcardManager.h"
#include <algorithm>
#include <QDialogButtonBox>
#include <QFileDialog>
#include <QGridLayout>
#include <QGroupBox>
#include <QHeaderView>
#include <QImage>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPixmap>
#include <QPushButton>
#include <QTableWidget>
#include <QTimer>
#include "Common/FileUtil.h"
#include "Common/MsgHandler.h"
#include "Common/StringUtil.h"
#include "Core/HW/GCMemcard/GCMemcard.h"
constexpr u32 BANNER_WIDTH = 96;
constexpr u32 ANIM_FRAME_WIDTH = 32;
constexpr u32 IMAGE_HEIGHT = 32;
constexpr u32 ANIM_MAX_FRAMES = 8;
constexpr float ROW_HEIGHT = 28;
GCMemcardManager::GCMemcardManager(QWidget* parent) : QDialog(parent)
{
CreateWidgets();
ConnectWidgets();
SetActiveSlot(0);
UpdateActions();
m_timer = new QTimer(this);
connect(m_timer, &QTimer::timeout, this, &GCMemcardManager::DrawIcons);
m_timer->start(1000 / 8);
// Make the dimensions more reasonable on startup
resize(650, 500);
}
GCMemcardManager::~GCMemcardManager() = default;
void GCMemcardManager::CreateWidgets()
{
m_button_box = new QDialogButtonBox(QDialogButtonBox::Ok);
// Actions
m_select_button = new QPushButton;
m_copy_button = new QPushButton;
// Contents will be set by their appropriate functions
m_delete_button = new QPushButton(tr("&Delete"));
m_export_button = new QPushButton(tr("&Export..."));
m_export_all_button = new QPushButton(tr("Export &All..."));
m_import_button = new QPushButton(tr("&Import..."));
m_fix_checksums_button = new QPushButton(tr("Fix Checksums"));
auto* layout = new QGridLayout;
for (int i = 0; i < SLOT_COUNT; i++)
{
m_slot_group[i] = new QGroupBox(i == 0 ? tr("Slot A") : tr("Slot B"));
m_slot_file_edit[i] = new QLineEdit;
m_slot_file_button[i] = new QPushButton(tr("&Browse..."));
m_slot_table[i] = new QTableWidget;
m_slot_stat_label[i] = new QLabel;
m_slot_table[i]->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_slot_table[i]->setSelectionBehavior(QAbstractItemView::SelectRows);
m_slot_table[i]->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
m_slot_table[i]->verticalHeader()->hide();
auto* slot_layout = new QGridLayout;
m_slot_group[i]->setLayout(slot_layout);
slot_layout->addWidget(m_slot_file_edit[i], 0, 0);
slot_layout->addWidget(m_slot_file_button[i], 0, 1);
slot_layout->addWidget(m_slot_table[i], 1, 0, 1, 2);
slot_layout->addWidget(m_slot_stat_label[i], 2, 0);
layout->addWidget(m_slot_group[i], 0, i * 2, 7, 1);
}
layout->addWidget(m_select_button, 0, 1);
layout->addWidget(m_copy_button, 1, 1);
layout->addWidget(m_delete_button, 2, 1);
layout->addWidget(m_export_button, 3, 1);
layout->addWidget(m_export_all_button, 4, 1);
layout->addWidget(m_import_button, 5, 1);
layout->addWidget(m_fix_checksums_button, 6, 1);
layout->addWidget(m_button_box, 7, 2);
setLayout(layout);
}
void GCMemcardManager::ConnectWidgets()
{
connect(m_button_box, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(m_select_button, &QPushButton::pressed, this, [this] { SetActiveSlot(!m_active_slot); });
connect(m_export_button, &QPushButton::pressed, this, [this] { ExportFiles(true); });
connect(m_export_all_button, &QPushButton::pressed, this, &GCMemcardManager::ExportAllFiles);
connect(m_delete_button, &QPushButton::pressed, this, &GCMemcardManager::DeleteFiles);
connect(m_import_button, &QPushButton::pressed, this, &GCMemcardManager::ImportFile);
connect(m_copy_button, &QPushButton::pressed, this, &GCMemcardManager::CopyFiles);
connect(m_fix_checksums_button, &QPushButton::pressed, this, &GCMemcardManager::FixChecksums);
for (int slot = 0; slot < SLOT_COUNT; slot++)
{
connect(m_slot_file_edit[slot], &QLineEdit::textChanged, this,
[this, slot](const QString& path) { SetSlotFile(slot, path); });
connect(m_slot_file_button[slot], &QPushButton::pressed, this,
[this, slot] { SetSlotFileInteractive(slot); });
connect(m_slot_table[slot], &QTableWidget::itemSelectionChanged, this,
&GCMemcardManager::UpdateActions);
}
}
void GCMemcardManager::SetActiveSlot(int slot)
{
for (int i = 0; i < SLOT_COUNT; i++)
m_slot_group[i]->setEnabled(i == slot);
m_select_button->setText(slot == 0 ? QStringLiteral("<") : QStringLiteral(">"));
m_copy_button->setText(slot == 0 ? QStringLiteral("Copy to B") : QStringLiteral("Copy to A"));
m_active_slot = slot;
UpdateSlotTable(slot);
UpdateActions();
}
void GCMemcardManager::UpdateSlotTable(int slot)
{
m_slot_active_icons[m_active_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)
return;
auto& memcard = m_slot_memcard[slot];
auto* table = m_slot_table[slot];
auto create_item = [this](const QString string = QStringLiteral("")) {
QTableWidgetItem* item = new QTableWidgetItem(string);
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
return item;
};
for (int i = 0; i < memcard->GetNumFiles(); i++)
{
int file_index = memcard->GetFileIndex(i);
table->setRowCount(i + 1);
auto const string_decoder = memcard->IsShiftJIS() ? SHIFTJISToUTF8 : CP1252ToUTF8;
QString title = QString::fromStdString(string_decoder(memcard->GetSaveComment1(file_index)));
QString comment = QString::fromStdString(string_decoder(memcard->GetSaveComment2(file_index)));
QString blocks = QStringLiteral("%1").arg(memcard->DEntry_BlockCount(file_index));
QString block_count = QStringLiteral("%1").arg(memcard->DEntry_FirstBlock(file_index));
auto* banner = new QTableWidgetItem;
banner->setData(Qt::DecorationRole, GetBannerFromSaveFile(file_index));
banner->setFlags(banner->flags() ^ Qt::ItemIsEditable);
auto frames = GetIconFromSaveFile(file_index);
auto* icon = new QTableWidgetItem;
icon->setData(Qt::DecorationRole, frames[0]);
DEntry d;
memcard->GetDEntry(file_index, d);
const auto speed = ((d.AnimSpeed[0] & 1) << 2) + (d.AnimSpeed[1] & 1);
m_slot_active_icons[m_active_slot].push_back({speed, frames});
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);
}
}
void GCMemcardManager::UpdateActions()
{
auto selection = m_slot_table[m_active_slot]->selectedItems();
bool have_selection = selection.count();
bool have_memcard = m_slot_memcard[m_active_slot] != nullptr;
bool have_memcard_other = m_slot_memcard[!m_active_slot] != nullptr;
m_copy_button->setEnabled(have_selection && have_memcard_other);
m_export_button->setEnabled(have_selection);
m_export_all_button->setEnabled(have_memcard);
m_import_button->setEnabled(have_memcard);
m_delete_button->setEnabled(have_selection);
m_fix_checksums_button->setEnabled(have_memcard);
}
void GCMemcardManager::SetSlotFile(int slot, QString path)
{
auto memcard = std::make_unique<GCMemcard>(path.toStdString());
if (!memcard->IsValid())
return;
m_slot_stat_label[slot]->setText(tr("%1 Free Blocks; %2 Free Dir Entries")
.arg(memcard->GetFreeBlocks())
.arg(DIRLEN - memcard->GetNumFiles()));
m_slot_file_edit[slot]->setText(path);
m_slot_memcard[slot] = std::move(memcard);
UpdateSlotTable(slot);
UpdateActions();
}
void GCMemcardManager::SetSlotFileInteractive(int slot)
{
QString path =
QFileDialog::getOpenFileName(this, slot == 0 ? tr("Set memory card file for Slot A") :
tr("Set memory card file for Slot B"),
QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)),
tr("GameCube Memory Cards (*.raw *.gcp)"));
if (!path.isEmpty())
m_slot_file_edit[slot]->setText(path);
}
void GCMemcardManager::ExportFiles(bool prompt)
{
auto selection = m_slot_table[m_active_slot]->selectedItems();
auto& memcard = m_slot_memcard[m_active_slot];
auto count = selection.count() / m_slot_table[m_active_slot]->columnCount();
for (int i = 0; i < count; i++)
{
auto sel = selection[i * m_slot_table[m_active_slot]->columnCount()];
int file_index = memcard->GetFileIndex(m_slot_table[m_active_slot]->row(sel));
std::string gci_filename;
if (!memcard->GCI_FileName(file_index, gci_filename))
return;
QString path;
if (prompt)
{
path = QFileDialog::getSaveFileName(
this, tr("Export Save File"),
QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)) +
QStringLiteral("/%1").arg(QString::fromStdString(gci_filename)),
tr("Native GCI File (*.gci)") + QStringLiteral(";;") +
tr("MadCatz Gameshark files(*.gcs)") + QStringLiteral(";;") +
tr("Datel MaxDrive/Pro files(*.sav)"));
if (path.isEmpty())
return;
}
else
{
path = QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)) +
QStringLiteral("/%1").arg(QString::fromStdString(gci_filename));
}
if (!memcard->ExportGci(file_index, path.toStdString(), ""))
{
File::Delete(path.toStdString());
}
}
QMessageBox::information(this, tr("Success"),
tr("Successfully exported %1 %2")
.arg(count)
.arg(count == 1 ? tr("save file") : tr("save files")));
}
void GCMemcardManager::ExportAllFiles()
{
// This is nothing but a thin wrapper around ExportFiles()
m_slot_table[m_active_slot]->selectAll();
ExportFiles(false);
}
void GCMemcardManager::ImportFile()
{
QString path = QFileDialog::getOpenFileName(
this, tr("Export Save File"), QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)),
tr("Native GCI File (*.gci)") + QStringLiteral(";;") + tr("MadCatz Gameshark files(*.gcs)") +
QStringLiteral(";;") + tr("Datel MaxDrive/Pro files(*.sav)"));
if (path.isEmpty())
return;
m_slot_memcard[m_active_slot]->ImportGci(path.toStdString(), "");
if (!m_slot_memcard[m_active_slot]->Save())
PanicAlertT("File write failed");
UpdateSlotTable(m_active_slot);
}
void GCMemcardManager::CopyFiles()
{
auto selection = m_slot_table[m_active_slot]->selectedItems();
auto& memcard = m_slot_memcard[m_active_slot];
auto count = selection.count() / m_slot_table[m_active_slot]->columnCount();
for (int i = 0; i < count; i++)
{
auto sel = selection[i * m_slot_table[m_active_slot]->columnCount()];
int file_index = memcard->GetFileIndex(m_slot_table[m_active_slot]->row(sel));
m_slot_memcard[!m_active_slot]->CopyFrom(*memcard, file_index);
}
if (!m_slot_memcard[!m_active_slot]->Save())
PanicAlertT("File write failed");
for (int i = 0; i < SLOT_COUNT; i++)
UpdateSlotTable(i);
}
void GCMemcardManager::DeleteFiles()
{
auto selection = m_slot_table[m_active_slot]->selectedItems();
auto& memcard = m_slot_memcard[m_active_slot];
auto count = selection.count() / m_slot_table[m_active_slot]->columnCount();
// Ask for confirmation if we are to delete multiple files
if (count > 1)
{
auto response = QMessageBox::warning(this, tr("Question"),
tr("Do you want to delete the %1 selected %2?")
.arg(count)
.arg(count == 1 ? tr("save file") : tr("save files")),
QMessageBox::Yes | QMessageBox::Abort);
if (response == QMessageBox::Abort)
return;
}
for (int i = 0; i < count; i++)
{
auto sel = selection[i * m_slot_table[m_active_slot]->columnCount()];
int file_index = memcard->GetFileIndex(m_slot_table[m_active_slot]->row(sel));
memcard->RemoveFile(file_index);
}
QMessageBox::information(this, tr("Success"), tr("Successfully deleted files."));
if (!memcard->Save())
PanicAlertT("File write failed");
UpdateSlotTable(m_active_slot);
UpdateActions();
}
void GCMemcardManager::FixChecksums()
{
auto& memcard = m_slot_memcard[m_active_slot];
memcard->FixChecksums();
if (!memcard->Save())
PanicAlertT("File write failed");
}
void GCMemcardManager::DrawIcons()
{
m_current_frame++;
m_current_frame %= 15;
int row = 0;
const auto column = 3;
for (auto& icon : m_slot_active_icons[m_active_slot])
{
int frame = (m_current_frame / 3 - icon.first) % icon.second.size();
auto* item = new QTableWidgetItem;
item->setData(Qt::DecorationRole, icon.second[frame]);
item->setFlags(item->flags() ^ Qt::ItemIsEditable);
m_slot_table[m_active_slot]->setItem(row, column, item);
row++;
}
}
QPixmap GCMemcardManager::GetBannerFromSaveFile(int file_index)
{
auto& memcard = m_slot_memcard[m_active_slot];
std::vector<u32> pxdata(BANNER_WIDTH * IMAGE_HEIGHT);
QImage image;
if (memcard->ReadBannerRGBA8(file_index, pxdata.data()))
{
image = QImage(reinterpret_cast<u8*>(pxdata.data()), BANNER_WIDTH, IMAGE_HEIGHT,
QImage::Format_ARGB32);
}
return QPixmap::fromImage(image);
}
std::vector<QPixmap> GCMemcardManager::GetIconFromSaveFile(int file_index)
{
auto& memcard = m_slot_memcard[m_active_slot];
std::vector<u32> pxdata(BANNER_WIDTH * IMAGE_HEIGHT);
std::vector<u8> anim_delay(ANIM_MAX_FRAMES);
std::vector<u32> anim_data(ANIM_FRAME_WIDTH * IMAGE_HEIGHT * ANIM_MAX_FRAMES);
std::vector<QPixmap> frame_pixmaps;
u32 num_frames = memcard->ReadAnimRGBA8(file_index, anim_data.data(), anim_delay.data());
// Decode Save File Animation
if (num_frames > 0)
{
u32 frames = BANNER_WIDTH / ANIM_FRAME_WIDTH;
if (num_frames < frames)
{
frames = num_frames;
// Clear unused frame's pixels from the buffer.
std::fill(pxdata.begin(), pxdata.end(), 0);
}
for (u32 f = 0; f < frames; ++f)
{
for (u32 y = 0; y < IMAGE_HEIGHT; ++y)
{
for (u32 x = 0; x < ANIM_FRAME_WIDTH; ++x)
{
// NOTE: pxdata is stacked horizontal, anim_data is stacked vertical
pxdata[y * BANNER_WIDTH + f * ANIM_FRAME_WIDTH + x] =
anim_data[f * ANIM_FRAME_WIDTH * IMAGE_HEIGHT + y * IMAGE_HEIGHT + x];
}
}
}
QImage anims(reinterpret_cast<u8*>(pxdata.data()), BANNER_WIDTH, IMAGE_HEIGHT,
QImage::Format_ARGB32);
for (u32 f = 0; f < frames; f++)
{
frame_pixmaps.push_back(
QPixmap::fromImage(anims.copy(ANIM_FRAME_WIDTH * f, 0, ANIM_FRAME_WIDTH, IMAGE_HEIGHT)));
}
}
else
{
// No Animation found, use an empty placeholder instead.
frame_pixmaps.push_back(QPixmap());
}
return frame_pixmaps;
}

View File

@ -0,0 +1,75 @@
// Copyright 2018 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#pragma once
#include <array>
#include <memory>
#include <utility>
#include <vector>
#include <QDialog>
class GCMemcard;
class QDialogButtonBox;
class QGroupBox;
class QLabel;
class QLineEdit;
class QPushButton;
class QTableWidget;
class QTimer;
class GCMemcardManager : public QDialog
{
public:
explicit GCMemcardManager(QWidget* parent = nullptr);
~GCMemcardManager();
private:
void CreateWidgets();
void ConnectWidgets();
void UpdateActions();
void UpdateSlotTable(int slot);
void SetSlotFile(int slot, QString path);
void SetSlotFileInteractive(int slot);
void SetActiveSlot(int slot);
void CopyFiles();
void ImportFile();
void DeleteFiles();
void ExportFiles(bool prompt);
void ExportAllFiles();
void FixChecksums();
void DrawIcons();
QPixmap GetBannerFromSaveFile(int file_index);
std::vector<QPixmap> GetIconFromSaveFile(int file_index);
// Actions
QPushButton* m_select_button;
QPushButton* m_copy_button;
QPushButton* m_export_button;
QPushButton* m_export_all_button;
QPushButton* m_import_button;
QPushButton* m_delete_button;
QPushButton* m_fix_checksums_button;
// Slots
static constexpr int SLOT_COUNT = 2;
std::array<std::vector<std::pair<int, std::vector<QPixmap>>>, SLOT_COUNT> m_slot_active_icons;
std::array<std::unique_ptr<GCMemcard>, SLOT_COUNT> m_slot_memcard;
std::array<QGroupBox*, SLOT_COUNT> m_slot_group;
std::array<QLineEdit*, SLOT_COUNT> m_slot_file_edit;
std::array<QPushButton*, SLOT_COUNT> m_slot_file_button;
std::array<QTableWidget*, SLOT_COUNT> m_slot_table;
std::array<QLabel*, SLOT_COUNT> m_slot_stat_label;
int m_active_slot;
int m_current_frame;
QDialogButtonBox* m_button_box;
QTimer* m_timer;
};

View File

@ -53,6 +53,7 @@
#include "DolphinQt2/Debugger/RegisterWidget.h" #include "DolphinQt2/Debugger/RegisterWidget.h"
#include "DolphinQt2/Debugger/WatchWidget.h" #include "DolphinQt2/Debugger/WatchWidget.h"
#include "DolphinQt2/FIFOPlayerWindow.h" #include "DolphinQt2/FIFOPlayerWindow.h"
#include "DolphinQt2/GCMemcardManager.h"
#include "DolphinQt2/Host.h" #include "DolphinQt2/Host.h"
#include "DolphinQt2/HotkeyScheduler.h" #include "DolphinQt2/HotkeyScheduler.h"
#include "DolphinQt2/MainWindow.h" #include "DolphinQt2/MainWindow.h"
@ -234,6 +235,7 @@ void MainWindow::ConnectMenuBar()
connect(m_menu_bar, &MenuBar::ConfigureHotkeys, this, &MainWindow::ShowHotkeyDialog); connect(m_menu_bar, &MenuBar::ConfigureHotkeys, this, &MainWindow::ShowHotkeyDialog);
// Tools // Tools
connect(m_menu_bar, &MenuBar::ShowMemcardManager, this, &MainWindow::ShowMemcardManager);
connect(m_menu_bar, &MenuBar::BootGameCubeIPL, this, &MainWindow::OnBootGameCubeIPL); connect(m_menu_bar, &MenuBar::BootGameCubeIPL, this, &MainWindow::OnBootGameCubeIPL);
connect(m_menu_bar, &MenuBar::ImportNANDBackup, this, &MainWindow::OnImportNANDBackup); connect(m_menu_bar, &MenuBar::ImportNANDBackup, this, &MainWindow::OnImportNANDBackup);
connect(m_menu_bar, &MenuBar::PerformOnlineUpdate, this, &MainWindow::PerformOnlineUpdate); connect(m_menu_bar, &MenuBar::PerformOnlineUpdate, this, &MainWindow::PerformOnlineUpdate);
@ -1065,3 +1067,10 @@ void MainWindow::OnConnectWiiRemote(int id)
Wiimote::Connect(id, !is_connected); Wiimote::Connect(id, !is_connected);
}); });
} }
void MainWindow::ShowMemcardManager()
{
GCMemcardManager manager(this);
manager.exec();
}

View File

@ -104,6 +104,7 @@ private:
void ShowHotkeyDialog(); void ShowHotkeyDialog();
void ShowNetPlaySetupDialog(); void ShowNetPlaySetupDialog();
void ShowFIFOPlayer(); void ShowFIFOPlayer();
void ShowMemcardManager();
void NetPlayInit(); void NetPlayInit();
bool NetPlayJoin(); bool NetPlayJoin();

View File

@ -109,6 +109,11 @@ void MenuBar::AddToolsMenu()
{ {
QMenu* tools_menu = addMenu(tr("&Tools")); QMenu* tools_menu = addMenu(tr("&Tools"));
AddAction(tools_menu, tr("&Memory Card Manager (GC)"), this,
[this] { emit ShowMemcardManager(); });
tools_menu->addSeparator();
AddAction(tools_menu, tr("Import Wii Save..."), this, &MenuBar::ImportWiiSave); AddAction(tools_menu, tr("Import Wii Save..."), this, &MenuBar::ImportWiiSave);
AddAction(tools_menu, tr("Export All Wii Saves"), this, &MenuBar::ExportWiiSaves); AddAction(tools_menu, tr("Export All Wii Saves"), this, &MenuBar::ExportWiiSaves);

View File

@ -62,6 +62,7 @@ signals:
void PerformOnlineUpdate(const std::string& region); void PerformOnlineUpdate(const std::string& region);
// Tools // Tools
void ShowMemcardManager();
void BootGameCubeIPL(DiscIO::Region region); void BootGameCubeIPL(DiscIO::Region region);
void ShowFIFOPlayer(); void ShowFIFOPlayer();
void ShowAboutDialog(); void ShowAboutDialog();