mirror of
https://github.com/melonDS-emu/melonDS.git
synced 2025-03-06 21:00:31 +01:00
Truncate the title at the first occurrence of \0, as title strings should be null-terminated. Fixes #2219 (Weird characters on DSi Title Manager on melonDS 1.0RC)
578 lines
18 KiB
C++
578 lines
18 KiB
C++
/*
|
|
Copyright 2016-2024 melonDS team
|
|
|
|
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 "main.h"
|
|
#include "DSi_NAND.h"
|
|
|
|
#include "TitleManagerDialog.h"
|
|
#include "ui_TitleManagerDialog.h"
|
|
#include "ui_TitleImportDialog.h"
|
|
|
|
using namespace melonDS;
|
|
using namespace melonDS::Platform;
|
|
|
|
std::unique_ptr<DSi_NAND::NANDImage> TitleManagerDialog::nand = nullptr;
|
|
TitleManagerDialog* TitleManagerDialog::currentDlg = nullptr;
|
|
|
|
|
|
TitleManagerDialog::TitleManagerDialog(QWidget* parent, DSi_NAND::NANDImage& image) : QDialog(parent), ui(new Ui::TitleManagerDialog), nandmount(image)
|
|
{
|
|
ui->setupUi(this);
|
|
setAttribute(Qt::WA_DeleteOnClose);
|
|
|
|
emuInstance = ((MainWindow*)parent)->getEmuInstance();
|
|
|
|
ui->lstTitleList->setIconSize(QSize(32, 32));
|
|
|
|
const u32 category = 0x00030004;
|
|
std::vector<u32> titlelist;
|
|
nandmount.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;
|
|
NDSHeader header;
|
|
NDSBanner banner;
|
|
|
|
nandmount.GetTitleInfo(category, titleid, version, &header, &banner);
|
|
|
|
u32 icondata[32*32];
|
|
emuInstance->romIcon(banner.Icon, banner.Palette, icondata);
|
|
QImage iconimg((const uchar*)icondata, 32, 32, QImage::Format_RGBA8888);
|
|
QIcon icon(QPixmap::fromImage(iconimg.copy()));
|
|
|
|
// TODO: make it possible to select other languages?
|
|
QString title = QString::fromUtf16(banner.EnglishTitle, 128);
|
|
title = title.left(title.indexOf('\0'));
|
|
title.replace("\n", " · ");
|
|
|
|
char gamecode[5];
|
|
*(u32*)&gamecode[0] = *(u32*)&header.GameCode[0];
|
|
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(header.DSiPublicSavSize)); // public.sav size
|
|
item->setData(Qt::UserRole+2, QVariant(header.DSiPrivateSavSize)); // private.sav size
|
|
item->setData(Qt::UserRole+3, QVariant((u32)((header.AppFlags & 0x04) ? 0x4000 : 0))); // banner.sav size
|
|
ui->lstTitleList->addItem(item);
|
|
}
|
|
|
|
bool TitleManagerDialog::openNAND()
|
|
{
|
|
nand = nullptr;
|
|
|
|
Config::Table cfg = Config::GetGlobalTable();
|
|
|
|
FileHandle* bios7i = Platform::OpenLocalFile(cfg.GetString("DSi.BIOS7Path"), FileMode::Read);
|
|
if (!bios7i)
|
|
return false;
|
|
|
|
u8 es_keyY[16];
|
|
FileSeek(bios7i, 0x8308, FileSeekOrigin::Start);
|
|
FileRead(es_keyY, 16, 1, bios7i);
|
|
CloseFile(bios7i);
|
|
|
|
FileHandle* nandfile = Platform::OpenLocalFile(cfg.GetString("DSi.NANDPath"), FileMode::ReadWriteExisting);
|
|
if (!nandfile)
|
|
return false;
|
|
|
|
nand = std::make_unique<DSi_NAND::NANDImage>(nandfile, es_keyY);
|
|
if (!*nand)
|
|
{ // If loading and mounting the NAND image failed...
|
|
nand = nullptr;
|
|
return false;
|
|
// NOTE: The NANDImage takes ownership of the FileHandle,
|
|
// so it will be closed even if the NANDImage constructor fails.
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void TitleManagerDialog::closeNAND()
|
|
{
|
|
nand = nullptr;
|
|
}
|
|
|
|
void TitleManagerDialog::done(int r)
|
|
{
|
|
QDialog::done(r);
|
|
|
|
closeDlg();
|
|
}
|
|
|
|
void TitleManagerDialog::on_btnImportTitle_clicked()
|
|
{
|
|
TitleImportDialog* importdlg = new TitleImportDialog(this, importAppPath, &importTmdData, importReadOnly, nandmount);
|
|
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.GetCategory();
|
|
titleid[1] = importTmdData.GetID();
|
|
|
|
assert(nand != nullptr);
|
|
assert(*nand);
|
|
// remove anything that might hinder the install
|
|
nandmount.DeleteTitle(titleid[0], titleid[1]);
|
|
|
|
bool importres = nandmount.ImportTitle(importAppPath.toStdString().c_str(), importTmdData, importReadOnly);
|
|
if (!importres)
|
|
{
|
|
// remove a potential half-completed install
|
|
nandmount.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();
|
|
nandmount.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)
|
|
{
|
|
Log(LogLevel::Error, "what??\n");
|
|
return;
|
|
}
|
|
|
|
QString extensions = "*.sav";
|
|
u32 wantedsize;
|
|
switch (type)
|
|
{
|
|
case DSi_NAND::TitleData_PublicSav:
|
|
extensions += " *.pub";
|
|
wantedsize = cur->data(Qt::UserRole+1).toUInt();
|
|
break;
|
|
case DSi_NAND::TitleData_PrivateSav:
|
|
extensions += " *.prv";
|
|
wantedsize = cur->data(Qt::UserRole+2).toUInt();
|
|
break;
|
|
case DSi_NAND::TitleData_BannerSav:
|
|
extensions += " *.bnr";
|
|
wantedsize = cur->data(Qt::UserRole+3).toUInt();
|
|
break;
|
|
default:
|
|
Log(LogLevel::Warn, "what??\n");
|
|
return;
|
|
}
|
|
|
|
QString file = QFileDialog::getOpenFileName(this,
|
|
"Select file to import...",
|
|
emuDirectory,
|
|
"Title data files (" + extensions + ");;Any file (*.*)");
|
|
|
|
if (file.isEmpty()) return;
|
|
|
|
Platform::FileHandle* f = Platform::OpenFile(file.toStdString(), Platform::Read);
|
|
if (!f)
|
|
{
|
|
QMessageBox::critical(this,
|
|
"Import title data - melonDS",
|
|
"Could not open data file.\nCheck that the file is accessible.");
|
|
return;
|
|
}
|
|
|
|
u64 len = Platform::FileLength(f);
|
|
Platform::CloseFile(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 = nandmount.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)
|
|
{
|
|
Log(LogLevel::Error, "what??\n");
|
|
return;
|
|
}
|
|
|
|
QString exportname;
|
|
QString extensions = "*.sav";
|
|
u32 wantedsize;
|
|
switch (type)
|
|
{
|
|
case DSi_NAND::TitleData_PublicSav:
|
|
exportname = "/public.sav";
|
|
extensions += " *.pub";
|
|
wantedsize = cur->data(Qt::UserRole+1).toUInt();
|
|
break;
|
|
case DSi_NAND::TitleData_PrivateSav:
|
|
exportname = "/private.sav";
|
|
extensions += " *.prv";
|
|
wantedsize = cur->data(Qt::UserRole+2).toUInt();
|
|
break;
|
|
case DSi_NAND::TitleData_BannerSav:
|
|
exportname = "/banner.sav";
|
|
extensions += " *.bnr";
|
|
wantedsize = cur->data(Qt::UserRole+3).toUInt();
|
|
break;
|
|
default:
|
|
Log(LogLevel::Warn, "what??\n");
|
|
return;
|
|
}
|
|
|
|
QString file = QFileDialog::getSaveFileName(this,
|
|
"Select path to export to...",
|
|
emuDirectory + exportname,
|
|
"Title data files (" + extensions + ");;Any file (*.*)");
|
|
|
|
if (file.isEmpty()) return;
|
|
|
|
u64 titleid = cur->data(Qt::UserRole).toULongLong();
|
|
bool res = nandmount.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, const DSi_TMD::TitleMetadata* tmd, bool& readonly, DSi_NAND::NANDMount& nandmount)
|
|
: QDialog(parent), ui(new Ui::TitleImportDialog), appPath(apppath), tmdData(tmd), readOnly(readonly), nandmount(nandmount)
|
|
{
|
|
ui->setupUi(this);
|
|
setAttribute(Qt::WA_DeleteOnClose);
|
|
|
|
grpTmdSource = new QButtonGroup(this);
|
|
grpTmdSource->addButton(ui->rbTmdFromFile, 0);
|
|
grpTmdSource->addButton(ui->rbTmdFromNUS, 1);
|
|
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
|
|
connect(grpTmdSource, SIGNAL(buttonClicked(int)), this, SLOT(onChangeTmdSource(int)));
|
|
#else
|
|
connect(grpTmdSource, SIGNAL(idClicked(int)), this, SLOT(onChangeTmdSource(int)));
|
|
#endif
|
|
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((void *) tmdData, sizeof(DSi_TMD::TitleMetadata), 1, f);
|
|
fclose(f);
|
|
|
|
u32 tmdtitleid[2];
|
|
tmdtitleid[0] = tmdData->GetCategory();
|
|
tmdtitleid[1] = tmdData->GetID();
|
|
|
|
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 (nandmount.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, sizeof(*tmdData));
|
|
|
|
u32 tmdtitleid[2];
|
|
tmdtitleid[0] = tmdData->GetCategory();
|
|
tmdtitleid[1] = tmdData->GetID();
|
|
|
|
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);
|
|
}
|