aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/qt_sdl
diff options
context:
space:
mode:
authorArisotura <thetotalworm@gmail.com>2021-08-24 17:46:20 +0200
committerGitHub <noreply@github.com>2021-08-24 17:46:20 +0200
commit235da420c8ac0bcec5a21db6f3cce4ae190fb7c5 (patch)
tree6aaacaa2b8c2f558601ea8321dd8141c8170f46c /src/frontend/qt_sdl
parent346e8c0b878541d987d3ee2bfe54b555f626f7bf (diff)
having fun with fatfs (#1189)
* patch TSC coords in DSi mode * DSiware importer and shit
Diffstat (limited to 'src/frontend/qt_sdl')
-rw-r--r--src/frontend/qt_sdl/CMakeLists.txt11
-rw-r--r--src/frontend/qt_sdl/TitleImportDialog.ui142
-rw-r--r--src/frontend/qt_sdl/TitleManagerDialog.cpp564
-rw-r--r--src/frontend/qt_sdl/TitleManagerDialog.h133
-rw-r--r--src/frontend/qt_sdl/TitleManagerDialog.ui133
-rw-r--r--src/frontend/qt_sdl/main.cpp17
-rw-r--r--src/frontend/qt_sdl/main.h2
7 files changed, 997 insertions, 5 deletions
diff --git a/src/frontend/qt_sdl/CMakeLists.txt b/src/frontend/qt_sdl/CMakeLists.txt
index 1ec01b1..3de536c 100644
--- a/src/frontend/qt_sdl/CMakeLists.txt
+++ b/src/frontend/qt_sdl/CMakeLists.txt
@@ -11,6 +11,7 @@ SET(SOURCES_QT_SDL
WifiSettingsDialog.cpp
InterfaceSettingsDialog.cpp
ROMInfoDialog.cpp
+ TitleManagerDialog.cpp
Input.cpp
LAN_PCap.cpp
LAN_Socket.cpp
@@ -46,11 +47,12 @@ if (USE_QT6)
set(Qt6Core_DIR ${QT6_STATIC_BASE}Core)
set(Qt6Gui_DIR ${QT6_STATIC_BASE}Gui)
set(Qt6Widgets_DIR ${QT6_STATIC_BASE}Widgets)
+ set(Qt6Network_DIR ${QT6_STATIC_BASE}Network)
set(Qt6OpenGL_DIR ${QT6_STATIC_BASE}OpenGL)
set(Qt6OpenGLWidgets_DIR ${QT6_STATIC_BASE}OpenGLWidgets)
endif()
- find_package(Qt6 COMPONENTS Core Gui Widgets OpenGL OpenGLWidgets REQUIRED)
- set(QT_LINK_LIBS Qt6::Core Qt6::Gui Qt6::Widgets Qt6::OpenGL Qt6::OpenGLWidgets)
+ find_package(Qt6 COMPONENTS Core Gui Widgets Network OpenGL OpenGLWidgets REQUIRED)
+ set(QT_LINK_LIBS Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Network Qt6::OpenGL Qt6::OpenGLWidgets)
else()
if (BUILD_STATIC AND QT5_STATIC_DIR)
set(QT5_STATIC_BASE ${QT5_STATIC_DIR}/lib/cmake/Qt5)
@@ -58,9 +60,10 @@ else()
set(Qt5Core_DIR ${QT5_STATIC_BASE}Core)
set(Qt5Gui_DIR ${QT5_STATIC_BASE}Gui)
set(Qt5Widgets_DIR ${QT5_STATIC_BASE}Widgets)
+ set(Qt5Network_DIR ${QT5_STATIC_BASE}Network)
endif()
- find_package(Qt5 COMPONENTS Core Gui Widgets REQUIRED)
- set(QT_LINK_LIBS Qt5::Core Qt5::Gui Qt5::Widgets)
+ find_package(Qt5 COMPONENTS Core Gui Widgets Network REQUIRED)
+ set(QT_LINK_LIBS Qt5::Core Qt5::Gui Qt5::Widgets Qt5::Network)
endif()
set(CMAKE_AUTOMOC ON)
diff --git a/src/frontend/qt_sdl/TitleImportDialog.ui b/src/frontend/qt_sdl/TitleImportDialog.ui
new file mode 100644
index 0000000..ca2010b
--- /dev/null
+++ b/src/frontend/qt_sdl/TitleImportDialog.ui
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>TitleImportDialog</class>
+ <widget class="QDialog" name="TitleImportDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>495</width>
+ <height>202</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="windowTitle">
+ <string>Import title - melonDS</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="3" column="2">
+ <widget class="QPushButton" name="btnTmdBrowse">
+ <property name="text">
+ <string>Browse...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="0" colspan="3">
+ <widget class="QRadioButton" name="rbTmdFromNUS">
+ <property name="text">
+ <string>Download from NUS</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0" colspan="3">
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Metadata (TMD):</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QRadioButton" name="rbTmdFromFile">
+ <property name="text">
+ <string>From file:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="txtAppFile"/>
+ </item>
+ <item row="7" column="0" colspan="3">
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0" colspan="3">
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Executable:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="0" colspan="3">
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QLineEdit" name="txtTmdFile"/>
+ </item>
+ <item row="0" column="2">
+ <widget class="QPushButton" name="btnAppBrowse">
+ <property name="text">
+ <string>Browse...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="0" colspan="3">
+ <widget class="QCheckBox" name="cbReadOnly">
+ <property name="whatsThis">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Makes the title executable and TMD read-only. Prevents DSi system utilities from deleting them.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Make title files read-only</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>TitleImportDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>TitleImportDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/src/frontend/qt_sdl/TitleManagerDialog.cpp b/src/frontend/qt_sdl/TitleManagerDialog.cpp
new file mode 100644
index 0000000..b51475f
--- /dev/null
+++ b/src/frontend/qt_sdl/TitleManagerDialog.cpp
@@ -0,0 +1,564 @@
+/*
+ Copyright 2016-2021 Arisotura
+
+ This file is part of melonDS.
+
+ melonDS is free software: you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option)
+ any later version.
+
+ melonDS is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with melonDS. If not, see http://www.gnu.org/licenses/.
+*/
+
+#include <stdio.h>
+#include <QFileDialog>
+#include <QMenu>
+
+#include "types.h"
+#include "Platform.h"
+#include "Config.h"
+#include "PlatformConfig.h"
+#include "FrontendUtil.h"
+#include "DSi_NAND.h"
+
+#include "TitleManagerDialog.h"
+#include "ui_TitleManagerDialog.h"
+#include "ui_TitleImportDialog.h"
+
+
+FILE* TitleManagerDialog::curNAND = nullptr;
+TitleManagerDialog* TitleManagerDialog::currentDlg = nullptr;
+
+extern char* EmuDirectory;
+
+
+TitleManagerDialog::TitleManagerDialog(QWidget* parent) : QDialog(parent), ui(new Ui::TitleManagerDialog)
+{
+ ui->setupUi(this);
+ setAttribute(Qt::WA_DeleteOnClose);
+
+ ui->lstTitleList->setIconSize(QSize(32, 32));
+
+ const u32 category = 0x00030004;
+ std::vector<u32> titlelist;
+ DSi_NAND::ListTitles(category, titlelist);
+
+ for (std::vector<u32>::iterator it = titlelist.begin(); it != titlelist.end(); it++)
+ {
+ u32 titleid = *it;
+ createTitleItem(category, titleid);
+ }
+
+ ui->lstTitleList->sortItems();
+
+ ui->btnImportTitleData->setEnabled(false);
+ ui->btnExportTitleData->setEnabled(false);
+ ui->btnDeleteTitle->setEnabled(false);
+
+ {
+ QMenu* menu = new QMenu(ui->btnImportTitleData);
+
+ actImportTitleData[0] = menu->addAction("public.sav");
+ actImportTitleData[0]->setData(QVariant(DSi_NAND::TitleData_PublicSav));
+ connect(actImportTitleData[0], &QAction::triggered, this, &TitleManagerDialog::onImportTitleData);
+
+ actImportTitleData[1] = menu->addAction("private.sav");
+ actImportTitleData[1]->setData(QVariant(DSi_NAND::TitleData_PrivateSav));
+ connect(actImportTitleData[1], &QAction::triggered, this, &TitleManagerDialog::onImportTitleData);
+
+ actImportTitleData[2] = menu->addAction("banner.sav");
+ actImportTitleData[2]->setData(QVariant(DSi_NAND::TitleData_BannerSav));
+ connect(actImportTitleData[2], &QAction::triggered, this, &TitleManagerDialog::onImportTitleData);
+
+ ui->btnImportTitleData->setMenu(menu);
+ }
+
+ {
+ QMenu* menu = new QMenu(ui->btnExportTitleData);
+
+ actExportTitleData[0] = menu->addAction("public.sav");
+ actExportTitleData[0]->setData(QVariant(DSi_NAND::TitleData_PublicSav));
+ connect(actExportTitleData[0], &QAction::triggered, this, &TitleManagerDialog::onExportTitleData);
+
+ actExportTitleData[1] = menu->addAction("private.sav");
+ actExportTitleData[1]->setData(QVariant(DSi_NAND::TitleData_PrivateSav));
+ connect(actExportTitleData[1], &QAction::triggered, this, &TitleManagerDialog::onExportTitleData);
+
+ actExportTitleData[2] = menu->addAction("banner.sav");
+ actExportTitleData[2]->setData(QVariant(DSi_NAND::TitleData_BannerSav));
+ connect(actExportTitleData[2], &QAction::triggered, this, &TitleManagerDialog::onExportTitleData);
+
+ ui->btnExportTitleData->setMenu(menu);
+ }
+}
+
+TitleManagerDialog::~TitleManagerDialog()
+{
+ delete ui;
+}
+
+void TitleManagerDialog::createTitleItem(u32 category, u32 titleid)
+{
+ u32 version;
+ u8 header[0x1000];
+ u8 banner[0x2400];
+
+ DSi_NAND::GetTitleInfo(category, titleid, version, header, banner);
+
+ u8 icongfx[512];
+ u16 iconpal[16];
+ memcpy(icongfx, &banner[0x20], 512);
+ memcpy(iconpal, &banner[0x220], 16*2);
+ u32 icondata[32*32];
+ Frontend::ROMIcon(icongfx, iconpal, icondata);
+ QImage iconimg((const uchar*)icondata, 32, 32, QImage::Format_ARGB32);
+ QIcon icon(QPixmap::fromImage(iconimg.copy()));
+
+ // TODO: make it possible to select other languages?
+ u16 titleraw[129];
+ memcpy(titleraw, &banner[0x340], 128*sizeof(u16));
+ titleraw[128] = '\0';
+ QString title = QString::fromUtf16(titleraw);
+ title.replace("\n", " · ");
+
+ char gamecode[5];
+ *(u32*)&gamecode[0] = *(u32*)&header[0xC];
+ gamecode[4] = '\0';
+ char extra[128];
+ sprintf(extra, "\n(title ID: %s · %08x/%08x · version %08x)", gamecode, category, titleid, version);
+
+ QListWidgetItem* item = new QListWidgetItem(title + QString(extra));
+ item->setIcon(icon);
+ item->setData(Qt::UserRole, QVariant((qulonglong)(((u64)category<<32) | (u64)titleid)));
+ item->setData(Qt::UserRole+1, QVariant(*(u32*)&header[0x238])); // public.sav size
+ item->setData(Qt::UserRole+2, QVariant(*(u32*)&header[0x23C])); // private.sav size
+ item->setData(Qt::UserRole+3, QVariant((u32)((header[0x1BF] & 0x04) ? 0x4000 : 0))); // banner.sav size
+ ui->lstTitleList->addItem(item);
+}
+
+bool TitleManagerDialog::openNAND()
+{
+ FILE* bios7i = Platform::OpenLocalFile(Config::DSiBIOS7Path, "rb");
+ if (!bios7i)
+ return false;
+
+ u8 es_keyY[16];
+ fseek(bios7i, 0x8308, SEEK_SET);
+ fread(es_keyY, 16, 1, bios7i);
+ fclose(bios7i);
+
+ curNAND = Platform::OpenLocalFile(Config::DSiNANDPath, "r+b");
+ if (!curNAND)
+ return false;
+
+ if (!DSi_NAND::Init(curNAND, es_keyY))
+ {
+ fclose(curNAND);
+ curNAND = nullptr;
+ return false;
+ }
+
+ return true;
+}
+
+void TitleManagerDialog::closeNAND()
+{
+ if (curNAND)
+ {
+ DSi_NAND::DeInit();
+
+ fclose(curNAND);
+ curNAND = nullptr;
+ }
+}
+
+void TitleManagerDialog::done(int r)
+{
+ QDialog::done(r);
+
+ closeDlg();
+}
+
+void TitleManagerDialog::on_btnImportTitle_clicked()
+{
+ TitleImportDialog* importdlg = new TitleImportDialog(this, importAppPath, importTmdData, importReadOnly);
+ importdlg->open();
+ connect(importdlg, &TitleImportDialog::finished, this, &TitleManagerDialog::onImportTitleFinished);
+
+ importdlg->show();
+}
+
+void TitleManagerDialog::onImportTitleFinished(int res)
+{
+ if (res != QDialog::Accepted) return;
+
+ u32 titleid[2];
+ titleid[0] = (importTmdData[0x18C] << 24) | (importTmdData[0x18D] << 16) | (importTmdData[0x18E] << 8) | importTmdData[0x18F];
+ titleid[1] = (importTmdData[0x190] << 24) | (importTmdData[0x191] << 16) | (importTmdData[0x192] << 8) | importTmdData[0x193];
+
+ // remove anything that might hinder the install
+ DSi_NAND::DeleteTitle(titleid[0], titleid[1]);
+
+ bool importres = DSi_NAND::ImportTitle(importAppPath.toStdString().c_str(), importTmdData, importReadOnly);
+ if (!importres)
+ {
+ // remove a potential half-completed install
+ DSi_NAND::DeleteTitle(titleid[0], titleid[1]);
+
+ QMessageBox::critical(this,
+ "Import title - melonDS",
+ "An error occured while installing the title to the NAND.\nCheck that your NAND dump is valid.");
+ }
+ else
+ {
+ // it worked, wee!
+ createTitleItem(titleid[0], titleid[1]);
+ ui->lstTitleList->sortItems();
+ }
+}
+
+void TitleManagerDialog::on_btnDeleteTitle_clicked()
+{
+ QListWidgetItem* cur = ui->lstTitleList->currentItem();
+ if (!cur) return;
+
+ if (QMessageBox::question(this,
+ "Delete title - melonDS",
+ "The title and its associated data will be permanently deleted. Are you sure?",
+ QMessageBox::StandardButtons(QMessageBox::Yes|QMessageBox::No),
+ QMessageBox::No) != QMessageBox::Yes)
+ return;
+
+ u64 titleid = cur->data(Qt::UserRole).toULongLong();
+ DSi_NAND::DeleteTitle((u32)(titleid >> 32), (u32)titleid);
+
+ delete cur;
+}
+
+void TitleManagerDialog::on_lstTitleList_currentItemChanged(QListWidgetItem* cur, QListWidgetItem* prev)
+{
+ if (!cur)
+ {
+ ui->btnImportTitleData->setEnabled(false);
+ ui->btnExportTitleData->setEnabled(false);
+ ui->btnDeleteTitle->setEnabled(false);
+ }
+ else
+ {
+ ui->btnImportTitleData->setEnabled(true);
+ ui->btnExportTitleData->setEnabled(true);
+ ui->btnDeleteTitle->setEnabled(true);
+
+ u32 val;
+ val = cur->data(Qt::UserRole+1).toUInt();
+ actImportTitleData[0]->setEnabled(val != 0);
+ actExportTitleData[0]->setEnabled(val != 0);
+ val = cur->data(Qt::UserRole+2).toUInt();
+ actImportTitleData[1]->setEnabled(val != 0);
+ actExportTitleData[1]->setEnabled(val != 0);
+ val = cur->data(Qt::UserRole+3).toUInt();
+ actImportTitleData[2]->setEnabled(val != 0);
+ actExportTitleData[2]->setEnabled(val != 0);
+ }
+}
+
+void TitleManagerDialog::onImportTitleData()
+{
+ int type = ((QAction*)sender())->data().toInt();
+
+ QListWidgetItem* cur = ui->lstTitleList->currentItem();
+ if (!cur)
+ {
+ printf("what??\n");
+ return;
+ }
+
+ u32 wantedsize;
+ switch (type)
+ {
+ case DSi_NAND::TitleData_PublicSav: wantedsize = cur->data(Qt::UserRole+1).toUInt(); break;
+ case DSi_NAND::TitleData_PrivateSav: wantedsize = cur->data(Qt::UserRole+2).toUInt(); break;
+ case DSi_NAND::TitleData_BannerSav: wantedsize = cur->data(Qt::UserRole+3).toUInt(); break;
+ default:
+ printf("what??\n");
+ return;
+ }
+
+ QString file = QFileDialog::getOpenFileName(this,
+ "Select file to import...",
+ EmuDirectory,
+ "Title data files (*.sav);;Any file (*.*)");
+
+ if (file.isEmpty()) return;
+
+ FILE* f = fopen(file.toStdString().c_str(), "rb");
+ if (!f)
+ {
+ QMessageBox::critical(this,
+ "Import title data - melonDS",
+ "Could not open data file.\nCheck that the file is accessible.");
+ return;
+ }
+
+ fseek(f, 0, SEEK_END);
+ u64 len = ftell(f);
+ fclose(f);
+
+ if (len != wantedsize)
+ {
+ QMessageBox::critical(this,
+ "Import title data - melonDS",
+ QString("Cannot import this data file: size is incorrect (expected: %1 bytes).").arg(wantedsize));
+ return;
+ }
+
+ u64 titleid = cur->data(Qt::UserRole).toULongLong();
+ bool res = DSi_NAND::ImportTitleData((u32)(titleid >> 32), (u32)titleid, type, file.toStdString().c_str());
+ if (!res)
+ {
+ QMessageBox::critical(this,
+ "Import title data - melonDS",
+ "Failed to import the data file. Check that your NAND is accessible and valid.");
+ }
+}
+
+void TitleManagerDialog::onExportTitleData()
+{
+ int type = ((QAction*)sender())->data().toInt();
+
+ QListWidgetItem* cur = ui->lstTitleList->currentItem();
+ if (!cur)
+ {
+ printf("what??\n");
+ return;
+ }
+
+ QString exportname;
+ u32 wantedsize;
+ switch (type)
+ {
+ case DSi_NAND::TitleData_PublicSav:
+ exportname = "/public.sav";
+ wantedsize = cur->data(Qt::UserRole+1).toUInt();
+ break;
+ case DSi_NAND::TitleData_PrivateSav:
+ exportname = "/private.sav";
+ wantedsize = cur->data(Qt::UserRole+2).toUInt();
+ break;
+ case DSi_NAND::TitleData_BannerSav:
+ exportname = "/banner.sav";
+ wantedsize = cur->data(Qt::UserRole+3).toUInt();
+ break;
+ default:
+ printf("what??\n");
+ return;
+ }
+
+ QString file = QFileDialog::getSaveFileName(this,
+ "Select path to export to...",
+ QString(EmuDirectory) + exportname,
+ "Title data files (*.sav);;Any file (*.*)");
+
+ if (file.isEmpty()) return;
+
+ u64 titleid = cur->data(Qt::UserRole).toULongLong();
+ bool res = DSi_NAND::ExportTitleData((u32)(titleid >> 32), (u32)titleid, type, file.toStdString().c_str());
+ if (!res)
+ {
+ QMessageBox::critical(this,
+ "Export title data - melonDS",
+ "Failed to Export the data file. Check that the destination directory is writable.");
+ }
+}
+
+
+TitleImportDialog::TitleImportDialog(QWidget* parent, QString& apppath, u8* tmd, bool& readonly)
+: QDialog(parent), ui(new Ui::TitleImportDialog), appPath(apppath), tmdData(tmd), readOnly(readonly)
+{
+ ui->setupUi(this);
+ setAttribute(Qt::WA_DeleteOnClose);
+
+ grpTmdSource = new QButtonGroup(this);
+ grpTmdSource->addButton(ui->rbTmdFromFile, 0);
+ grpTmdSource->addButton(ui->rbTmdFromNUS, 1);
+ connect(grpTmdSource, SIGNAL(buttonClicked(int)), this, SLOT(onChangeTmdSource(int)));
+ grpTmdSource->button(0)->setChecked(true);
+}
+
+TitleImportDialog::~TitleImportDialog()
+{
+ delete ui;
+}
+
+void TitleImportDialog::accept()
+{
+ QString path;
+ FILE* f;
+
+ bool tmdfromfile = (grpTmdSource->checkedId() == 0);
+
+ path = ui->txtAppFile->text();
+ f = fopen(path.toStdString().c_str(), "rb");
+ if (!f)
+ {
+ QMessageBox::critical(this,
+ "Import title - melonDS",
+ "Could not open executable file.\nCheck that the path is correct and that the file is accessible.");
+ return;
+ }
+
+ fseek(f, 0x230, SEEK_SET);
+ fread(titleid, 8, 1, f);
+ fclose(f);
+
+ if (titleid[1] != 0x00030004)
+ {
+ QMessageBox::critical(this,
+ "Import title - melonDS",
+ "Executable file is not a DSiware title.");
+ return;
+ }
+
+ if (tmdfromfile)
+ {
+ path = ui->txtTmdFile->text();
+ f = fopen(path.toStdString().c_str(), "rb");
+ if (!f)
+ {
+ QMessageBox::critical(this,
+ "Import title - melonDS",
+ "Could not open metadata file.\nCheck that the path is correct and that the file is accessible.");
+ return;
+ }
+
+ fread(tmdData, 0x208, 1, f);
+ fclose(f);
+
+ u32 tmdtitleid[2];
+ tmdtitleid[0] = (tmdData[0x18C] << 24) | (tmdData[0x18D] << 16) | (tmdData[0x18E] << 8) | tmdData[0x18F];
+ tmdtitleid[1] = (tmdData[0x190] << 24) | (tmdData[0x191] << 16) | (tmdData[0x192] << 8) | tmdData[0x193];
+
+ if (tmdtitleid[1] != titleid[0] || tmdtitleid[0] != titleid[1])
+ {
+ QMessageBox::critical(this,
+ "Import title - melonDS",
+ "Title ID in metadata file does not match executable file.");
+ return;
+ }
+ }
+
+ if (DSi_NAND::TitleExists(titleid[1], titleid[0]))
+ {
+ if (QMessageBox::question(this,
+ "Import title - melonDS",
+ "The selected title is already installed. Overwrite it?",
+ QMessageBox::StandardButtons(QMessageBox::Yes|QMessageBox::No),
+ QMessageBox::No) != QMessageBox::Yes)
+ return;
+ }
+
+ if (!tmdfromfile)
+ {
+ network = new QNetworkAccessManager(this);
+
+ char url[256];
+ sprintf(url, "http://nus.cdn.t.shop.nintendowifi.net/ccs/download/%08x%08x/tmd", titleid[1], titleid[0]);
+
+ QNetworkRequest req;
+ req.setUrl(QUrl(url));
+
+ netreply = network->get(req);
+ connect(netreply, &QNetworkReply::finished, this, &TitleImportDialog::tmdDownloaded);
+
+ setEnabled(false);
+ }
+ else
+ {
+ appPath = ui->txtAppFile->text();
+ readOnly = ui->cbReadOnly->isChecked();
+ QDialog::accept();
+ }
+}
+
+void TitleImportDialog::tmdDownloaded()
+{
+ bool good = false;
+
+ if (netreply->error() != QNetworkReply::NoError)
+ {
+ QMessageBox::critical(this,
+ "Import title - melonDS",
+ QString("An error occurred while trying to download the metadata file:\n\n") + netreply->errorString());
+ }
+ else if (netreply->bytesAvailable() < 2312)
+ {
+ QMessageBox::critical(this,
+ "Import title - melonDS",
+ "NUS returned a malformed metadata file.");
+ }
+ else
+ {
+ netreply->read((char*)tmdData, 520);
+
+ u32 tmdtitleid[2];
+ tmdtitleid[0] = (tmdData[0x18C] << 24) | (tmdData[0x18D] << 16) | (tmdData[0x18E] << 8) | tmdData[0x18F];
+ tmdtitleid[1] = (tmdData[0x190] << 24) | (tmdData[0x191] << 16) | (tmdData[0x192] << 8) | tmdData[0x193];
+
+ if (tmdtitleid[1] != titleid[0] || tmdtitleid[0] != titleid[1])
+ {
+ QMessageBox::critical(this,
+ "Import title - melonDS",
+ "NUS returned a malformed metadata file.");
+ }
+ else
+ good = true;
+ }
+
+ netreply->deleteLater();
+ setEnabled(true);
+
+ if (good)
+ {
+ appPath = ui->txtAppFile->text();
+ readOnly = ui->cbReadOnly->isChecked();
+ QDialog::accept();
+ }
+}
+
+void TitleImportDialog::on_btnAppBrowse_clicked()
+{
+ QString file = QFileDialog::getOpenFileName(this,
+ "Select title executable...",
+ EmuDirectory,
+ "DSiware executables (*.app *.nds *.dsi *.srl);;Any file (*.*)");
+
+ if (file.isEmpty()) return;
+
+ ui->txtAppFile->setText(file);
+}
+
+void TitleImportDialog::on_btnTmdBrowse_clicked()
+{
+ QString file = QFileDialog::getOpenFileName(this,
+ "Select title metadata...",
+ EmuDirectory,
+ "DSiware metadata (*.tmd);;Any file (*.*)");
+
+ if (file.isEmpty()) return;
+
+ ui->txtTmdFile->setText(file);
+}
+
+void TitleImportDialog::onChangeTmdSource(int id)
+{
+ bool pathenable = (id==0);
+
+ ui->txtTmdFile->setEnabled(pathenable);
+ ui->btnTmdBrowse->setEnabled(pathenable);
+}
diff --git a/src/frontend/qt_sdl/TitleManagerDialog.h b/src/frontend/qt_sdl/TitleManagerDialog.h
new file mode 100644
index 0000000..682362a
--- /dev/null
+++ b/src/frontend/qt_sdl/TitleManagerDialog.h
@@ -0,0 +1,133 @@
+/*
+ Copyright 2016-2021 Arisotura
+
+ This file is part of melonDS.
+
+ melonDS is free software: you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option)
+ any later version.
+
+ melonDS is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with melonDS. If not, see http://www.gnu.org/licenses/.
+*/
+
+#ifndef TITLEMANAGERDIALOG_H
+#define TITLEMANAGERDIALOG_H
+
+#include <QDialog>
+#include <QMessageBox>
+#include <QListWidget>
+#include <QButtonGroup>
+
+#include <QUrl>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QNetworkAccessManager>
+
+namespace Ui
+{
+ class TitleManagerDialog;
+ class TitleImportDialog;
+}
+class TitleManagerDialog;
+class TitleImportDialog;
+
+class TitleManagerDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit TitleManagerDialog(QWidget* parent);
+ ~TitleManagerDialog();
+
+ static FILE* curNAND;
+ static bool openNAND();
+ static void closeNAND();
+
+ static TitleManagerDialog* currentDlg;
+ static TitleManagerDialog* openDlg(QWidget* parent)
+ {
+ if (currentDlg)
+ {
+ currentDlg->activateWindow();
+ return currentDlg;
+ }
+
+ if (!openNAND())
+ {
+ QMessageBox::critical(parent,
+ "DSi title manager - melonDS",
+ "Failed to mount the DSi NAND. Check that your NAND dump is accessible and valid.");
+ return nullptr;
+ }
+
+ currentDlg = new TitleManagerDialog(parent);
+ currentDlg->open();
+ return currentDlg;
+ }
+ static void closeDlg()
+ {
+ currentDlg = nullptr;
+ closeNAND();
+ }
+
+private slots:
+ void done(int r);
+
+ void on_btnImportTitle_clicked();
+ void onImportTitleFinished(int res);
+ void on_btnDeleteTitle_clicked();
+ void on_lstTitleList_currentItemChanged(QListWidgetItem* cur, QListWidgetItem* prev);
+ void onImportTitleData();
+ void onExportTitleData();
+
+private:
+ Ui::TitleManagerDialog* ui;
+
+ QString importAppPath;
+ u8 importTmdData[0x208];
+ bool importReadOnly;
+
+ QAction* actImportTitleData[3];
+ QAction* actExportTitleData[3];
+
+ void createTitleItem(u32 category, u32 titleid);
+};
+
+class TitleImportDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit TitleImportDialog(QWidget* parent, QString& apppath, u8* tmd, bool& readonly);
+ ~TitleImportDialog();
+
+private slots:
+ void accept() override;
+ void tmdDownloaded();
+
+ void on_btnAppBrowse_clicked();
+ void on_btnTmdBrowse_clicked();
+ void onChangeTmdSource(int id);
+
+private:
+ Ui::TitleImportDialog* ui;
+
+ QButtonGroup* grpTmdSource;
+
+ QNetworkAccessManager* network;
+ QNetworkReply* netreply;
+
+ QString& appPath;
+ u8* tmdData;
+ bool& readOnly;
+
+ u32 titleid[2];
+};
+
+#endif // TITLEMANAGERDIALOG_H
diff --git a/src/frontend/qt_sdl/TitleManagerDialog.ui b/src/frontend/qt_sdl/TitleManagerDialog.ui
new file mode 100644
index 0000000..8e84681
--- /dev/null
+++ b/src/frontend/qt_sdl/TitleManagerDialog.ui
@@ -0,0 +1,133 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>TitleManagerDialog</class>
+ <widget class="QDialog" name="TitleManagerDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>608</width>
+ <height>466</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>DSi title manager - melonDS</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <property name="leftMargin">
+ <number>6</number>
+ </property>
+ <property name="topMargin">
+ <number>6</number>
+ </property>
+ <property name="rightMargin">
+ <number>6</number>
+ </property>
+ <property name="bottomMargin">
+ <number>6</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QPushButton" name="btnImportTitle">
+ <property name="whatsThis">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Import a DSiware title to your emulated DSi system.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Import title...</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btnImportTitleData">
+ <property name="whatsThis">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Import data (save, banner...) for the selected title.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Import title data</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btnExportTitleData">
+ <property name="whatsThis">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Export the data (save, banner...) associated with the selected title.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Export title data</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btnDeleteTitle">
+ <property name="whatsThis">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Permanently delete the selected title and its associated data from your emulated DSi.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Delete title</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QListWidget" name="lstTitleList"/>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Close</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>TitleManagerDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>303</x>
+ <y>448</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>303</x>
+ <y>232</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>TitleManagerDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>303</x>
+ <y>448</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>303</x>
+ <y>232</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/src/frontend/qt_sdl/main.cpp b/src/frontend/qt_sdl/main.cpp
index 1146a84..73fcf5a 100644
--- a/src/frontend/qt_sdl/main.cpp
+++ b/src/frontend/qt_sdl/main.cpp
@@ -58,6 +58,7 @@
#include "WifiSettingsDialog.h"
#include "InterfaceSettingsDialog.h"
#include "ROMInfoDialog.h"
+#include "TitleManagerDialog.h"
#include "types.h"
#include "version.h"
@@ -1377,8 +1378,11 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent)
connect(actSetupCheats, &QAction::triggered, this, &MainWindow::onSetupCheats);
menu->addSeparator();
- actROMInfo = menu->addAction("ROM Info");
+ actROMInfo = menu->addAction("ROM info");
connect(actROMInfo, &QAction::triggered, this, &MainWindow::onROMInfo);
+
+ actTitleManager = menu->addAction("Manage DSi titles");
+ connect(actTitleManager, &QAction::triggered, this, &MainWindow::onOpenTitleManager);
}
{
QMenu* menu = menubar->addMenu("Config");
@@ -1580,6 +1584,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent)
actFrameStep->setEnabled(false);
actSetupCheats->setEnabled(false);
+ actTitleManager->setEnabled(strlen(Config::DSiNANDPath) > 0);
actEnableCheats->setChecked(Config::EnableCheats != 0);
@@ -2391,6 +2396,11 @@ void MainWindow::onROMInfo()
ROMInfoDialog* dlg = ROMInfoDialog::openDlg(this);
}
+void MainWindow::onOpenTitleManager()
+{
+ TitleManagerDialog* dlg = TitleManagerDialog::openDlg(this);
+}
+
void MainWindow::onOpenEmuSettings()
{
emuThread->emuPause();
@@ -2405,6 +2415,9 @@ void MainWindow::onEmuSettingsDialogFinished(int res)
if (EmuSettingsDialog::needsReset)
onReset();
+
+ if (!RunningSomething)
+ actTitleManager->setEnabled(strlen(Config::DSiNANDPath) > 0);
}
void MainWindow::onOpenInputConfig()
@@ -2661,6 +2674,7 @@ void MainWindow::onEmuStart()
actImportSavefile->setEnabled(true);
actSetupCheats->setEnabled(true);
+ actTitleManager->setEnabled(false);
actROMInfo->setEnabled(true);
}
@@ -2683,6 +2697,7 @@ void MainWindow::onEmuStop()
actFrameStep->setEnabled(false);
actSetupCheats->setEnabled(false);
+ actTitleManager->setEnabled(strlen(Config::DSiNANDPath) > 0);
actROMInfo->setEnabled(false);
}
diff --git a/src/frontend/qt_sdl/main.h b/src/frontend/qt_sdl/main.h
index 2b93543..1378417 100644
--- a/src/frontend/qt_sdl/main.h
+++ b/src/frontend/qt_sdl/main.h
@@ -248,6 +248,7 @@ private slots:
void onSetupCheats();
void onCheatsDialogFinished(int res);
void onROMInfo();
+ void onOpenTitleManager();
void onOpenEmuSettings();
void onEmuSettingsDialogFinished(int res);
@@ -323,6 +324,7 @@ public:
QAction* actEnableCheats;
QAction* actSetupCheats;
QAction* actROMInfo;
+ QAction* actTitleManager;
QAction* actEmuSettings;
QAction* actInputConfig;