diff --git a/src/DSi_NAND.cpp b/src/DSi_NAND.cpp
index 58a0d278..304ede89 100644
--- a/src/DSi_NAND.cpp
+++ b/src/DSi_NAND.cpp
@@ -509,22 +509,25 @@ void debug_listfiles(const char* path)
f_closedir(&dir);
}
-void DumpFile(const char* path, const char* out)
+bool ImportFile(const char* path, const char* in)
{
FIL file;
- FILE* fout;
+ FILE* fin;
FRESULT res;
- res = f_open(&file, path, FA_OPEN_EXISTING | FA_READ);
- if (res != FR_OK) return;
+ fin = fopen(in, "rb");
+ if (!fin)
+ return false;
- u32 len = f_size(&file);
+ fseek(fin, 0, SEEK_END);
+ u32 len = (u32)ftell(fin);
+ fseek(fin, 0, SEEK_SET);
- fout = fopen(out, "wb");
- if (!fout)
+ res = f_open(&file, path, FA_CREATE_ALWAYS | FA_WRITE);
+ if (res != FR_OK)
{
- f_close(&file);
- return;
+ fclose(fin);
+ return false;
}
u8 buf[0x200];
@@ -536,13 +539,54 @@ void DumpFile(const char* path, const char* out)
else
blocklen = 0x200;
- u32 burp;
- f_read(&file, buf, blocklen, &burp);
+ u32 nwrite;
+ fread(buf, blocklen, 1, fin);
+ f_write(&file, buf, blocklen, &nwrite);
+ }
+
+ fclose(fin);
+ f_close(&file);
+
+ return true;
+}
+
+bool ExportFile(const char* path, const char* out)
+{
+ FIL file;
+ FILE* fout;
+ FRESULT res;
+
+ res = f_open(&file, path, FA_OPEN_EXISTING | FA_READ);
+ if (res != FR_OK)
+ return false;
+
+ u32 len = f_size(&file);
+
+ fout = fopen(out, "wb");
+ if (!fout)
+ {
+ f_close(&file);
+ return false;
+ }
+
+ u8 buf[0x200];
+ for (u32 i = 0; i < len; i += 0x200)
+ {
+ u32 blocklen;
+ if ((i + 0x200) > len)
+ blocklen = len - i;
+ else
+ blocklen = 0x200;
+
+ u32 nread;
+ f_read(&file, buf, blocklen, &nread);
fwrite(buf, blocklen, 1, fout);
}
fclose(fout);
f_close(&file);
+
+ return true;
}
void RemoveFile(const char* path)
@@ -692,6 +736,9 @@ bool TitleExists(u32 category, u32 titleid)
void GetTitleInfo(u32 category, u32 titleid, u32& version, u8* header, u8* banner)
{
version = GetTitleVersion(category, titleid);
+ if (version == 0xFFFFFFFF)
+ return;
+
FRESULT res;
char path[256];
@@ -704,15 +751,18 @@ void GetTitleInfo(u32 category, u32 titleid, u32& version, u8* header, u8* banne
u32 nread;
f_read(&file, header, 0x1000, &nread);
- u32 banneraddr = *(u32*)&header[0x68];
- if (!banneraddr)
+ if (banner)
{
- memset(banner, 0, 0x2400);
- }
- else
- {
- f_lseek(&file, banneraddr);
- f_read(&file, banner, 0x2400, &nread);
+ u32 banneraddr = *(u32*)&header[0x68];
+ if (!banneraddr)
+ {
+ memset(banner, 0, 0x2400);
+ }
+ else
+ {
+ f_lseek(&file, banneraddr);
+ f_read(&file, banner, 0x2400, &nread);
+ }
}
f_close(&file);
@@ -916,29 +966,12 @@ bool ImportTitle(const char* appfile, u8* tmd, bool readonly)
// executable
sprintf(fname, "0:/title/%08x/%08x/content/%08x.app", titleid0, titleid1, version);
- res = f_open(&file, fname, FA_CREATE_ALWAYS | FA_WRITE);
- if (res != FR_OK)
+ if (!ImportFile(fname, appfile))
{
printf("ImportTitle: failed to create executable (%d)\n", res);
return false;
}
- FILE* app = fopen(appfile, "rb");
- fseek(app, 0, SEEK_END);
- u32 applen = (u32)ftell(app);
- fseek(app, 0, SEEK_SET);
-
- for (u32 i = 0; i < applen; i += 0x200)
- {
- u8 data[0x200];
-
- u32 lenread = fread(data, 1, 0x200, app);
- f_write(&file, data, lenread, &nwrite);
- }
-
- fclose(app);
- f_close(&file);
-
if (readonly) f_chmod(fname, AM_RDO, AM_RDO);
return true;
@@ -955,4 +988,72 @@ void DeleteTitle(u32 category, u32 titleid)
RemoveDir(fname);
}
+u32 GetTitleDataMask(u32 category, u32 titleid)
+{
+ u32 version;
+ u8 header[0x1000];
+
+ GetTitleInfo(category, titleid, version, header, nullptr);
+ if (version == 0xFFFFFFFF)
+ return 0;
+
+ u32 ret = 0;
+ if (*(u32*)&header[0x238] != 0) ret |= (1 << TitleData_PublicSav);
+ if (*(u32*)&header[0x23C] != 0) ret |= (1 << TitleData_PrivateSav);
+ if (header[0x1BF] & 0x04) ret |= (1 << TitleData_BannerSav);
+
+ return ret;
+}
+
+bool ImportTitleData(u32 category, u32 titleid, int type, const char* file)
+{
+ char fname[128];
+
+ switch (type)
+ {
+ case TitleData_PublicSav:
+ sprintf(fname, "0:/title/%08x/%08x/data/public.sav", category, titleid);
+ break;
+
+ case TitleData_PrivateSav:
+ sprintf(fname, "0:/title/%08x/%08x/data/private.sav", category, titleid);
+ break;
+
+ case TitleData_BannerSav:
+ sprintf(fname, "0:/title/%08x/%08x/data/banner.sav", category, titleid);
+ break;
+
+ default:
+ return false;
+ }
+
+ RemoveFile(fname);
+ return ImportFile(fname, file);
+}
+
+bool ExportTitleData(u32 category, u32 titleid, int type, const char* file)
+{
+ char fname[128];
+
+ switch (type)
+ {
+ case TitleData_PublicSav:
+ sprintf(fname, "0:/title/%08x/%08x/data/public.sav", category, titleid);
+ break;
+
+ case TitleData_PrivateSav:
+ sprintf(fname, "0:/title/%08x/%08x/data/private.sav", category, titleid);
+ break;
+
+ case TitleData_BannerSav:
+ sprintf(fname, "0:/title/%08x/%08x/data/banner.sav", category, titleid);
+ break;
+
+ default:
+ return false;
+ }
+
+ return ExportFile(fname, file);
+}
+
}
diff --git a/src/DSi_NAND.h b/src/DSi_NAND.h
index 41898928..fbd75934 100644
--- a/src/DSi_NAND.h
+++ b/src/DSi_NAND.h
@@ -26,6 +26,13 @@
namespace DSi_NAND
{
+enum
+{
+ TitleData_PublicSav,
+ TitleData_PrivateSav,
+ TitleData_BannerSav,
+};
+
bool Init(FILE* nand, u8* es_keyY);
void DeInit();
@@ -39,6 +46,10 @@ void GetTitleInfo(u32 category, u32 titleid, u32& version, u8* header, u8* banne
bool ImportTitle(const char* appfile, u8* tmd, bool readonly);
void DeleteTitle(u32 category, u32 titleid);
+u32 GetTitleDataMask(u32 category, u32 titleid);
+bool ImportTitleData(u32 category, u32 titleid, int type, const char* file);
+bool ExportTitleData(u32 category, u32 titleid, int type, const char* file);
+
}
#endif // DSI_NAND_H
diff --git a/src/frontend/qt_sdl/TitleImportDialog.ui b/src/frontend/qt_sdl/TitleImportDialog.ui
index 4fa78cab..ca2010b6 100644
--- a/src/frontend/qt_sdl/TitleImportDialog.ui
+++ b/src/frontend/qt_sdl/TitleImportDialog.ui
@@ -94,6 +94,9 @@
-
+
+ <html><head/><body><p>Makes the title executable and TMD read-only. Prevents DSi system utilities from deleting them.</p></body></html>
+
Make title files read-only
diff --git a/src/frontend/qt_sdl/TitleManagerDialog.cpp b/src/frontend/qt_sdl/TitleManagerDialog.cpp
index ff0cec34..117b8b94 100644
--- a/src/frontend/qt_sdl/TitleManagerDialog.cpp
+++ b/src/frontend/qt_sdl/TitleManagerDialog.cpp
@@ -18,6 +18,7 @@
#include
#include
+#include
#include "types.h"
#include "Platform.h"
@@ -59,6 +60,42 @@ TitleManagerDialog::TitleManagerDialog(QWidget* parent) : QDialog(parent), ui(ne
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()
@@ -99,6 +136,9 @@ void TitleManagerDialog::createTitleItem(u32 category, u32 titleid)
QListWidgetItem* item = new QListWidgetItem(title + QString(extra));
item->setIcon(icon);
item->setData(Qt::UserRole, (((u64)category<<32) | (u64)titleid));
+ item->setData(Qt::UserRole+1, *(u32*)&header[0x238]); // public.sav size
+ item->setData(Qt::UserRole+2, *(u32*)&header[0x23C]); // private.sav size
+ item->setData(Qt::UserRole+3, (u32)((header[0x1BF] & 0x04) ? 0x4000 : 0)); // banner.sav size
ui->lstTitleList->addItem(item);
}
@@ -183,16 +223,6 @@ void TitleManagerDialog::onImportTitleFinished(int res)
}
}
-void TitleManagerDialog::on_btnImportTitleData_clicked()
-{
- //
-}
-
-void TitleManagerDialog::on_btnExportTitleData_clicked()
-{
- //
-}
-
void TitleManagerDialog::on_btnDeleteTitle_clicked()
{
QListWidgetItem* cur = ui->lstTitleList->currentItem();
@@ -205,7 +235,7 @@ void TitleManagerDialog::on_btnDeleteTitle_clicked()
QMessageBox::No) != QMessageBox::Yes)
return;
- u64 titleid = cur->data(Qt::UserRole).toLongLong();
+ u64 titleid = cur->data(Qt::UserRole).toULongLong();
DSi_NAND::DeleteTitle((u32)(titleid >> 32), (u32)titleid);
delete cur;
@@ -225,7 +255,125 @@ void TitleManagerDialog::on_lstTitleList_currentItemChanged(QListWidgetItem* cur
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.");
}
}
diff --git a/src/frontend/qt_sdl/TitleManagerDialog.h b/src/frontend/qt_sdl/TitleManagerDialog.h
index e0f80cb8..682362ab 100644
--- a/src/frontend/qt_sdl/TitleManagerDialog.h
+++ b/src/frontend/qt_sdl/TitleManagerDialog.h
@@ -81,10 +81,10 @@ private slots:
void on_btnImportTitle_clicked();
void onImportTitleFinished(int res);
- void on_btnImportTitleData_clicked();
- void on_btnExportTitleData_clicked();
void on_btnDeleteTitle_clicked();
void on_lstTitleList_currentItemChanged(QListWidgetItem* cur, QListWidgetItem* prev);
+ void onImportTitleData();
+ void onExportTitleData();
private:
Ui::TitleManagerDialog* ui;
@@ -93,8 +93,8 @@ private:
u8 importTmdData[0x208];
bool importReadOnly;
- QAction* importAction[3];
- QAction* exportAction[3];
+ QAction* actImportTitleData[3];
+ QAction* actExportTitleData[3];
void createTitleItem(u32 category, u32 titleid);
};
diff --git a/src/frontend/qt_sdl/TitleManagerDialog.ui b/src/frontend/qt_sdl/TitleManagerDialog.ui
index 3d177d72..be52752d 100644
--- a/src/frontend/qt_sdl/TitleManagerDialog.ui
+++ b/src/frontend/qt_sdl/TitleManagerDialog.ui
@@ -44,7 +44,7 @@
<html><head/><body><p>Import data (save, banner...) for the selected title.</p></body></html>
- Import title data...
+ Import title data
@@ -54,7 +54,7 @@
<html><head/><body><p>Export the data (save, banner...) associated with the selected title.</p></body></html>
- Export title data...
+ Export title data