diff --git a/src/frontend/qt_sdl/CLI.cpp b/src/frontend/qt_sdl/CLI.cpp index 5d8ebd10bf..58e386e8ff 100644 --- a/src/frontend/qt_sdl/CLI.cpp +++ b/src/frontend/qt_sdl/CLI.cpp @@ -39,7 +39,7 @@ CommandLineOptions* ManageArgs(QApplication& melon) parser.addOption(QCommandLineOption({"b", "boot"}, "Whether to boot firmware on startup. Defaults to \"auto\" (boot if NDS rom given)", "auto/always/never", "auto")); parser.addOption(QCommandLineOption({"f", "fullscreen"}, "Start melonDS in fullscreen mode")); - + #ifdef ARCHIVE_SUPPORT_ENABLED parser.addOption(QCommandLineOption({"a", "archive-file"}, "Specify file to load inside an archive given (NDS)", "rom")); parser.addOption(QCommandLineOption({"A", "archive-file-gba"}, "Specify file to load inside an archive given (GBA)", "rom")); @@ -50,16 +50,16 @@ CommandLineOptions* ManageArgs(QApplication& melon) CommandLineOptions* options = new CommandLineOptions; options->fullscreen = parser.isSet("fullscreen"); - + QStringList posargs = parser.positionalArguments(); switch (posargs.size()) { default: printf("Too many positional arguments; ignoring 3 onwards\n"); case 2: - options->gbaRomPath = QStringList(posargs[1]); + options->gbaRomPath = posargs[1]; case 1: - options->dsRomPath = QStringList(posargs[0]); + options->dsRomPath = posargs[0]; case 0: break; } @@ -67,8 +67,8 @@ CommandLineOptions* ManageArgs(QApplication& melon) QString bootMode = parser.value("boot"); if (bootMode == "auto") { - options->boot = posargs.size() > 0; - } + options->boot = !posargs.empty(); + } else if (bootMode == "always") { options->boot = true; @@ -86,45 +86,25 @@ CommandLineOptions* ManageArgs(QApplication& melon) #ifdef ARCHIVE_SUPPORT_ENABLED if (parser.isSet("archive-file")) { - if (options->dsRomPath.isEmpty()) + if (options->dsRomPath.has_value()) { - options->errorsToDisplay += "Option -a/--archive-file given, but no archive specified!"; + options->dsRomArchivePath = parser.value("archive-file"); } else { - options->dsRomPath += parser.value("archive-file"); - } - } - else if (!options->dsRomPath.isEmpty()) - { - //TODO-CLI: try to automatically find ROM - QStringList paths = options->dsRomPath[0].split("|"); - if (paths.size() >= 2) - { - printf("Warning: use the a.zip|b.nds format at your own risk!\n"); - options->dsRomPath = paths; + options->errorsToDisplay += "Option -a/--archive-file given, but no archive specified!"; } } if (parser.isSet("archive-file-gba")) { - if (options->gbaRomPath.isEmpty()) + if (options->gbaRomPath.has_value()) { - options->errorsToDisplay += "Option -A/--archive-file-gba given, but no archive specified!"; + options->gbaRomArchivePath = parser.value("archive-file-gba"); } else { - options->gbaRomPath += parser.value("archive-file-gba"); - } - } - else if (!options->gbaRomPath.isEmpty()) - { - //TODO-CLI: try to automatically find ROM - QStringList paths = options->gbaRomPath[0].split("|"); - if (paths.size() >= 2) - { - printf("Warning: use the a.zip|b.gba format at your own risk!\n"); - options->gbaRomPath = paths; + options->errorsToDisplay += "Option -A/--archive-file-gba given, but no archive specified!"; } } #endif @@ -132,4 +112,4 @@ CommandLineOptions* ManageArgs(QApplication& melon) return options; } -} \ No newline at end of file +} diff --git a/src/frontend/qt_sdl/CLI.h b/src/frontend/qt_sdl/CLI.h index 18520faecd..8850fad203 100644 --- a/src/frontend/qt_sdl/CLI.h +++ b/src/frontend/qt_sdl/CLI.h @@ -11,7 +11,7 @@ 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/. */ @@ -22,14 +22,18 @@ #include #include +#include + namespace CLI { struct CommandLineOptions { QStringList errorsToDisplay = {}; - QStringList dsRomPath; - QStringList gbaRomPath; + std::optional dsRomPath; + std::optional dsRomArchivePath; + std::optional gbaRomPath; + std::optional gbaRomArchivePath; bool fullscreen; bool boot; }; diff --git a/src/frontend/qt_sdl/main.cpp b/src/frontend/qt_sdl/main.cpp index 8ba8fb6c41..6c26060f35 100644 --- a/src/frontend/qt_sdl/main.cpp +++ b/src/frontend/qt_sdl/main.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -29,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -99,6 +101,55 @@ // TODO: uniform variable spelling +const QString NdsRomMimeType = "application/x-nintendo-ds-rom"; +const QStringList NdsRomExtensions { ".nds", ".srl", ".dsi", ".ids" }; + +const QString GbaRomMimeType = "application/x-gba-rom"; +const QStringList GbaRomExtensions { ".gba", ".agb" }; + +// This list of supported archive formats is based on libarchive(3) version 3.6.2 (2022-12-09). +const QStringList ArchiveMimeTypes +{ +#ifdef ARCHIVE_SUPPORT_ENABLED + "application/zip", + "application/x-7z-compressed", + "application/vnd.rar", // *.rar + "application/x-tar", + + "application/x-compressed-tar", // *.tar.gz + "application/x-xz-compressed-tar", + "application/x-bzip-compressed-tar", + "application/x-lz4-compressed-tar", + "application/x-zstd-compressed-tar", + + "application/x-tarz", // *.tar.Z + "application/x-lzip-compressed-tar", + "application/x-lzma-compressed-tar", + "application/x-lrzip-compressed-tar", + "application/x-tzo", // *.tar.lzo +#endif +}; + +const QStringList ArchiveExtensions +{ +#ifdef ARCHIVE_SUPPORT_ENABLED + ".zip", ".7z", ".rar", ".tar", + + ".tar.gz", ".tgz", + ".tar.xz", ".txz", + ".tar.bz2", ".tbz2", + ".tar.lz4", ".tlz4", + ".tar.zst", ".tzst", + + ".tar.Z", ".taz", + ".tar.lz", + ".tar.lzma", ".tlz", + ".tar.lrz", ".tlrz", + ".tar.lzo", ".tzo", +#endif +}; + + bool RunningSomething; MainWindow* mainWindow; @@ -588,7 +639,7 @@ void EmuThread::run() #endif { videoRenderer = 0; - } + } videoRenderer = oglContext ? Config::_3DRenderer : 0; @@ -1418,6 +1469,65 @@ void ScreenPanelGL::onScreenLayoutChanged() setupScreenLayout(); } + +static bool FileExtensionInList(const QString& filename, const QStringList& extensions, Qt::CaseSensitivity cs = Qt::CaseInsensitive) +{ + return std::any_of(extensions.cbegin(), extensions.cend(), [&](const auto& ext) { + return filename.endsWith(ext, cs); + }); +} + +static bool MimeTypeInList(const QMimeType& mimetype, const QStringList& superTypeNames) +{ + return std::any_of(superTypeNames.cbegin(), superTypeNames.cend(), [&](const auto& superTypeName) { + return mimetype.inherits(superTypeName); + }); +} + + +static bool NdsRomByExtension(const QString& filename) +{ + return FileExtensionInList(filename, NdsRomExtensions); +} + +static bool GbaRomByExtension(const QString& filename) +{ + return FileExtensionInList(filename, GbaRomExtensions); +} + +static bool SupportedArchiveByExtension(const QString& filename) +{ + return FileExtensionInList(filename, ArchiveExtensions); +} + + +static bool NdsRomByMimetype(const QMimeType& mimetype) +{ + return mimetype.inherits(NdsRomMimeType); +} + +static bool GbaRomByMimetype(const QMimeType& mimetype) +{ + return mimetype.inherits(GbaRomMimeType); +} + +static bool SupportedArchiveByMimetype(const QMimeType& mimetype) +{ + return MimeTypeInList(mimetype, ArchiveMimeTypes); +} + + +static bool FileIsSupportedFiletype(const QString& filename, bool insideArchive = false) +{ + if (NdsRomByExtension(filename) || GbaRomByExtension(filename) || SupportedArchiveByExtension(filename)) + return true; + + const auto matchmode = insideArchive ? QMimeDatabase::MatchExtension : QMimeDatabase::MatchDefault; + const QMimeType mimetype = QMimeDatabase().mimeTypeForFile(filename, matchmode); + return NdsRomByMimetype(mimetype) || GbaRomByMimetype(mimetype) || SupportedArchiveByMimetype(mimetype); +} + + #ifndef _WIN32 static int signalFd[2]; QSocketNotifier *signalSn; @@ -2014,14 +2124,8 @@ void MainWindow::dragEnterEvent(QDragEnterEvent* event) QString filename = urls.at(0).toLocalFile(); - QStringList acceptedExts{".nds", ".srl", ".dsi", ".gba", ".rar", - ".zip", ".7z", ".tar", ".tar.gz", ".tar.xz", ".tar.bz2"}; - - for (const QString &ext : acceptedExts) - { - if (filename.endsWith(ext, Qt::CaseInsensitive)) - event->acceptProposedAction(); - } + if (FileIsSupportedFiletype(filename)) + event->acceptProposedAction(); } void MainWindow::dropEvent(QDropEvent* event) @@ -2031,9 +2135,6 @@ void MainWindow::dropEvent(QDropEvent* event) QList urls = event->mimeData()->urls(); if (urls.count() > 1) return; // not handling more than one file at once - QString filename = urls.at(0).toLocalFile(); - QStringList arcexts{".zip", ".7z", ".rar", ".tar", ".tar.gz", ".tar.xz", ".tar.bz2"}; - emuThread->emuPause(); if (!verifySetup()) @@ -2042,55 +2143,57 @@ void MainWindow::dropEvent(QDropEvent* event) return; } - for (const QString &ext : arcexts) + const QStringList file = splitArchivePath(urls.at(0).toLocalFile(), false); + if (file.isEmpty()) { - if (filename.endsWith(ext, Qt::CaseInsensitive)) - { - QString arcfile = pickFileFromArchive(filename); - if (arcfile.isEmpty()) - { - emuThread->emuUnpause(); - return; - } - - filename += "|" + arcfile; - } + emuThread->emuUnpause(); + return; } - QStringList file = filename.split('|'); + const QString filename = file.last(); + const bool romInsideArchive = file.size() > 1; + const auto matchMode = romInsideArchive ? QMimeDatabase::MatchExtension : QMimeDatabase::MatchDefault; + const QMimeType mimetype = QMimeDatabase().mimeTypeForFile(filename, matchMode); - if (filename.endsWith(".gba", Qt::CaseInsensitive)) + if (NdsRomByExtension(filename) || NdsRomByMimetype(mimetype)) { - if (!ROMManager::LoadGBAROM(file)) + if (!ROMManager::LoadROM(file, true)) { // TODO: better error reporting? - QMessageBox::critical(this, "melonDS", "Failed to load the ROM."); + QMessageBox::critical(this, "melonDS", "Failed to load the DS ROM."); emuThread->emuUnpause(); return; } - emuThread->emuUnpause(); + const QString barredFilename = file.join('|'); + recentFileList.removeAll(barredFilename); + recentFileList.prepend(barredFilename); + updateRecentFilesMenu(); - updateCartInserted(true); + NDS::Start(); + emuThread->emuRun(); + + updateCartInserted(false); } - else + else if (GbaRomByExtension(filename) || GbaRomByMimetype(mimetype)) { - if (!ROMManager::LoadROM(file, true)) + if (!ROMManager::LoadGBAROM(file)) { // TODO: better error reporting? - QMessageBox::critical(this, "melonDS", "Failed to load the ROM."); + QMessageBox::critical(this, "melonDS", "Failed to load the GBA ROM."); emuThread->emuUnpause(); return; } - recentFileList.removeAll(filename); - recentFileList.prepend(filename); - updateRecentFilesMenu(); - - NDS::Start(); - emuThread->emuRun(); + emuThread->emuUnpause(); - updateCartInserted(false); + updateCartInserted(true); + } + else + { + QMessageBox::critical(this, "melonDS", "The file could not be recognized as a DS or GBA ROM."); + emuThread->emuUnpause(); + return; } } @@ -2188,101 +2291,129 @@ bool MainWindow::preloadROMs(QStringList file, QStringList gbafile, bool boot) return true; } -QString MainWindow::pickFileFromArchive(QString archiveFileName) +QStringList MainWindow::splitArchivePath(const QString& filename, bool useMemberSyntax) { - QVector archiveROMList = Archive::ListArchive(archiveFileName); + if (filename.isEmpty()) return {}; - QString romFileName = ""; // file name inside archive - - if (archiveROMList.size() > 2) +#ifdef ARCHIVE_SUPPORT_ENABLED + if (useMemberSyntax) { - archiveROMList.removeFirst(); + const QStringList filenameParts = filename.split('|'); + if (filenameParts.size() > 2) + { + QMessageBox::warning(this, "melonDS", "This path contains too many '|'."); + return {}; + } - bool ok; - QString toLoad = QInputDialog::getItem(this, "melonDS", - "This archive contains multiple files. Select which ROM you want to load.", archiveROMList.toList(), 0, false, &ok); - if (!ok) // User clicked on cancel - return QString(); + if (filenameParts.size() == 2) + { + const QString archive = filenameParts.at(0); + if (!QFileInfo(archive).exists()) + { + QMessageBox::warning(this, "melonDS", "This archive does not exist."); + return {}; + } - romFileName = toLoad; - } - else if (archiveROMList.size() == 2) - { - romFileName = archiveROMList.at(1); + const QString subfile = filenameParts.at(1); + if (!Archive::ListArchive(archive).contains(subfile)) + { + QMessageBox::warning(this, "melonDS", "This archive does not contain the desired file."); + return {}; + } + + return filenameParts; + } } - else if ((archiveROMList.size() == 1) && (archiveROMList[0] == QString("OK"))) +#endif + + if (!QFileInfo(filename).exists()) { - QMessageBox::warning(this, "melonDS", "This archive is empty."); + QMessageBox::warning(this, "melonDS", "This ROM file does not exist."); + return {}; } - else + +#ifdef ARCHIVE_SUPPORT_ENABLED + if (SupportedArchiveByExtension(filename) + || SupportedArchiveByMimetype(QMimeDatabase().mimeTypeForFile(filename))) { - QMessageBox::critical(this, "melonDS", "This archive could not be read. It may be corrupt or you don't have the permissions."); + const QString subfile = pickFileFromArchive(filename); + if (subfile.isEmpty()) + return {}; + + return { filename, subfile }; } +#endif - return romFileName; + return { filename }; } -QStringList MainWindow::pickROM(bool gba) +QString MainWindow::pickFileFromArchive(QString archiveFileName) { - QString console; - QStringList romexts; - QStringList arcexts{"*.zip", "*.7z", "*.rar", "*.tar", "*.tar.gz", "*.tar.xz", "*.tar.bz2"}; - QStringList ret; + QVector archiveROMList = Archive::ListArchive(archiveFileName); - if (gba) + if (archiveROMList.size() <= 1) { - console = "GBA"; - romexts.append("*.gba"); + if (!archiveROMList.isEmpty() && archiveROMList.at(0) == "OK") + QMessageBox::warning(this, "melonDS", "This archive is empty."); + else + QMessageBox::critical(this, "melonDS", "This archive could not be read. It may be corrupt or you don't have the permissions."); + return QString(); } - else + + archiveROMList.removeFirst(); + + const auto notSupportedRom = [&](const auto& filename){ + if (NdsRomByExtension(filename) || GbaRomByExtension(filename)) + return false; + const QMimeType mimetype = QMimeDatabase().mimeTypeForFile(filename, QMimeDatabase::MatchExtension); + return !(NdsRomByMimetype(mimetype) || GbaRomByMimetype(mimetype)); + }; + + archiveROMList.erase(std::remove_if(archiveROMList.begin(), archiveROMList.end(), notSupportedRom), + archiveROMList.end()); + + if (archiveROMList.isEmpty()) { - console = "DS"; - romexts.append({"*.nds", "*.dsi", "*.ids", "*.srl"}); + QMessageBox::warning(this, "melonDS", "This archive does not contain any supported ROMs."); + return QString(); } - QString filter = romexts.join(' ') + " " + arcexts.join(' '); - filter = console + " ROMs (" + filter + ");;Any file (*.*)"; + if (archiveROMList.size() == 1) + return archiveROMList.first(); - QString filename = QFileDialog::getOpenFileName(this, - "Open "+console+" ROM", - QString::fromStdString(Config::LastROMFolder), - filter); - if (filename.isEmpty()) - return ret; + bool ok; + const QString toLoad = QInputDialog::getItem( + this, "melonDS", + "This archive contains multiple files. Select which ROM you want to load.", + archiveROMList.toList(), 0, false, &ok + ); - int pos = filename.length() - 1; - while (filename[pos] != '/' && filename[pos] != '\\' && pos > 0) pos--; - QString path_dir = filename.left(pos); - QString path_file = filename.mid(pos+1); + if (ok) return toLoad; - Config::LastROMFolder = path_dir.toStdString(); + // User clicked on cancel - bool isarc = false; - for (const auto& ext : arcexts) - { - int l = ext.length() - 1; - if (path_file.right(l).toLower() == ext.right(l)) - { - isarc = true; - break; - } - } + return QString(); +} - if (isarc) - { - path_file = pickFileFromArchive(filename); - if (path_file.isEmpty()) - return ret; +QStringList MainWindow::pickROM(bool gba) +{ + const QString console = gba ? "GBA" : "DS"; + const QStringList& romexts = gba ? GbaRomExtensions : NdsRomExtensions; - ret.append(filename); - ret.append(path_file); - } - else - { - ret.append(filename); - } + static const QString filterSuffix = ArchiveExtensions.empty() + ? ");;Any file (*.*)" + : " *" + ArchiveExtensions.join(" *") + ");;Any file (*.*)"; - return ret; + const QString filename = QFileDialog::getOpenFileName( + this, "Open " + console + " ROM", + QString::fromStdString(Config::LastROMFolder), + console + " ROMs (*" + romexts.join(" *") + filterSuffix + ); + + if (filename.isEmpty()) return {}; + + Config::LastROMFolder = QFileInfo(filename).dir().path().toStdString(); + return splitArchivePath(filename, false); } void MainWindow::updateCartInserted(bool gba) @@ -2405,7 +2536,6 @@ void MainWindow::onClickRecentFile() { QAction *act = (QAction *)sender(); QString filename = act->data().toString(); - QStringList file = filename.split('|'); emuThread->emuPause(); @@ -2415,6 +2545,13 @@ void MainWindow::onClickRecentFile() return; } + const QStringList file = splitArchivePath(filename, true); + if (file.isEmpty()) + { + emuThread->emuUnpause(); + return; + } + if (!ROMManager::LoadROM(file, true)) { // TODO: better error reporting? @@ -3237,7 +3374,8 @@ bool MelonApplication::event(QEvent *event) QFileOpenEvent *openEvent = static_cast(event); emuThread->emuPause(); - if (!mainWindow->preloadROMs(openEvent->file().split("|"), {}, true)) + const QStringList file = mainWindow->splitArchivePath(openEvent->file(), true); + if (!mainWindow->preloadROMs(file, {}, true)) emuThread->emuUnpause(); } @@ -3256,7 +3394,7 @@ int main(int argc, char** argv) // easter egg - not worth checking other cases for something so dumb if (argc != 0 && (!strcasecmp(argv[0], "derpDS") || !strcasecmp(argv[0], "./derpDS"))) printf("did you just call me a derp???\n"); - + Platform::Init(argc, argv); MelonApplication melon(argc, argv); @@ -3379,7 +3517,26 @@ int main(int argc, char** argv) QObject::connect(&melon, &QApplication::applicationStateChanged, mainWindow, &MainWindow::onAppStateChanged); - mainWindow->preloadROMs(options->dsRomPath, options->gbaRomPath, options->boot); + bool memberSyntaxUsed = false; + const auto prepareRomPath = [&](const std::optional& romPath, const std::optional& romArchivePath) -> QStringList + { + if (!romPath.has_value()) + return {}; + + if (romArchivePath.has_value()) + return { *romPath, *romArchivePath }; + + const QStringList path = mainWindow->splitArchivePath(*romPath, true); + if (path.size() > 1) memberSyntaxUsed = true; + return path; + }; + + const QStringList dsfile = prepareRomPath(options->dsRomPath, options->dsRomArchivePath); + const QStringList gbafile = prepareRomPath(options->gbaRomPath, options->gbaRomArchivePath); + + if (memberSyntaxUsed) printf("Warning: use the a.zip|b.nds format at your own risk!\n"); + + mainWindow->preloadROMs(dsfile, gbafile, options->boot); int ret = melon.exec(); diff --git a/src/frontend/qt_sdl/main.h b/src/frontend/qt_sdl/main.h index 9f9fc7c050..30af909b66 100644 --- a/src/frontend/qt_sdl/main.h +++ b/src/frontend/qt_sdl/main.h @@ -239,6 +239,7 @@ class MainWindow : public QMainWindow GL::Context* getOGLContext(); bool preloadROMs(QStringList file, QStringList gbafile, bool boot); + QStringList splitArchivePath(const QString& filename, bool useMemberSyntax); void onAppStateChanged(Qt::ApplicationState state);