From 32059bfaa2aeab9dece0a32d196cddb6622e2994 Mon Sep 17 00:00:00 2001 From: NicknineTheEagle Date: Tue, 4 Dec 2018 01:46:01 +0300 Subject: [PATCH] Properly get PARAM.SFO and icons for C00 games (#5370) * Added a helper function for fetching game's PARAM.SFO path This should properly get SFO path for unlocked C00 games * Normalized line endings * Refresh game list after installing a RAP file --- rpcs3/Emu/RSX/Overlays/overlays.h | 9 +- rpcs3/Emu/System.cpp | 73 +++++--- rpcs3/Emu/System.h | 9 +- rpcs3/rpcs3qt/game_list_frame.cpp | 270 ++++++++++++++---------------- rpcs3/rpcs3qt/main_window.cpp | 50 +++--- 5 files changed, 216 insertions(+), 195 deletions(-) diff --git a/rpcs3/Emu/RSX/Overlays/overlays.h b/rpcs3/Emu/RSX/Overlays/overlays.h index f6353187c1..14cdd8b783 100644 --- a/rpcs3/Emu/RSX/Overlays/overlays.h +++ b/rpcs3/Emu/RSX/Overlays/overlays.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "overlay_controls.h" #include "../../../Utilities/date_time.h" @@ -813,14 +813,11 @@ namespace rsx if (use_custom_background) { - std::string root_path = Emu.GetBoot(); - root_path = root_path.substr(0, root_path.find_last_of("/")); - - auto icon_path = root_path + "/../PIC1.PNG"; + auto icon_path = Emu.GetSfoDir() + "/PIC1.PNG"; if (!fs::exists(icon_path)) { // Fallback path - icon_path = root_path + "/../ICON0.PNG"; + icon_path = Emu.GetSfoDir() + "/ICON0.PNG"; } if (fs::exists(icon_path)) diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 8bf588856c..7201c531c6 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -1,4 +1,4 @@ -#include "stdafx.h" +#include "stdafx.h" #include "Utilities/bin_patch.h" #include "Emu/Memory/vm.h" #include "Emu/System.h" @@ -571,6 +571,32 @@ std::string Emulator::GetHddDir() return fmt::replace_all(g_cfg.vfs.dev_hdd0, "$(EmulatorDir)", GetEmuDir()); } +std::string Emulator::GetSfoDirFromGamePath(const std::string& game_path, const std::string& user) +{ + if (fs::is_file(game_path + "/PS3_DISC.SFB")) + { + // This is a disc game. + return game_path + "/PS3_GAME"; + } + + const auto psf = psf::load_object(fs::file(game_path + "/PARAM.SFO")); + + const std::string category = get_string(psf, "CATEGORY"); + const std::string content_id = get_string(psf, "CONTENT_ID"); + if (category == "HG" && !content_id.empty()) + { + // This is a trial game. Check if the user has a RAP file to unlock it. + const std::string rap_path = GetHddDir() + "home/" + user + "/exdata/" + content_id + ".rap"; + if (fs::is_file(rap_path) && fs::is_file(game_path + "/C00/PARAM.SFO")) + { + // Load full game data. + return game_path + "/C00"; + } + } + + return game_path; +} + void Emulator::SetForceBoot(bool force_boot) { m_force_boot = force_boot; @@ -600,42 +626,43 @@ void Emulator::Load(bool add_only) const std::string elf_dir = fs::get_parent_dir(m_path); // Load PARAM.SFO (TODO) - const auto _psf = psf::load_object([&]() -> fs::file + psf::registry _psf; + if (fs::file sfov{elf_dir + "/sce_sys/param.sfo"}) + { + m_sfo_dir = elf_dir; + _psf = psf::load_object(sfov); + } + else { - if (fs::file sfov{elf_dir + "/sce_sys/param.sfo"}) - { - return sfov; - } - if (fs::is_dir(m_path)) { // Special case (directory scan) - if (fs::file sfo{m_path + "/PS3_GAME/PARAM.SFO"}) - { - return sfo; - } - - return fs::file{m_path + "/PARAM.SFO"}; + m_sfo_dir = GetSfoDirFromGamePath(m_path, GetUsr()); } - - if (disc.size()) + else if (disc.size()) { // Check previously used category before it's overwritten if (m_cat == "DG") { - return fs::file{disc + "/PS3_GAME/PARAM.SFO"}; + m_sfo_dir = disc + "/PS3_GAME"; } - - if (m_cat == "GD") + else if (m_cat == "GD") { - return fs::file{GetHddDir() + "game/" + m_title_id + "/PARAM.SFO"}; + m_sfo_dir = GetHddDir() + "game/" + m_title_id; } - - return fs::file{disc + "/PARAM.SFO"}; + else + { + m_sfo_dir = GetSfoDirFromGamePath(disc, GetUsr()); + } + } + else + { + m_sfo_dir = GetSfoDirFromGamePath(fs::get_parent_dir(elf_dir), GetUsr()); } - return fs::file{elf_dir + "/../PARAM.SFO"}; - }()); + _psf = psf::load_object(fs::file(m_sfo_dir + "/PARAM.SFO")); + } + m_title = psf::get_string(_psf, "TITLE", m_path.substr(m_path.find_last_of('/') + 1)); m_title_id = psf::get_string(_psf, "TITLE_ID"); m_cat = psf::get_string(_psf, "CATEGORY"); diff --git a/rpcs3/Emu/System.h b/rpcs3/Emu/System.h index dc615136b6..d2b162795c 100644 --- a/rpcs3/Emu/System.h +++ b/rpcs3/Emu/System.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "VFS.h" #include "Utilities/Atomic.h" @@ -215,6 +215,7 @@ class Emulator final std::string m_title; std::string m_cat; std::string m_dir; + std::string m_sfo_dir; std::string m_usr{"00000001"}; u32 m_usrid{1}; @@ -285,6 +286,11 @@ public: return m_dir; } + const std::string& GetSfoDir() const + { + return m_sfo_dir; + } + // String for GUI dialogs. const std::string& GetUsr() const { @@ -312,6 +318,7 @@ private: static std::string GetEmuDir(); public: static std::string GetHddDir(); + static std::string GetSfoDirFromGamePath(const std::string& game_path, const std::string& user); void SetForceBoot(bool force_boot); diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index ad5edc9594..4f372f147c 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -1,4 +1,4 @@ -#include "game_list_frame.h" +#include "game_list_frame.h" #include "qt_utils.h" #include "settings_dialog.h" #include "table_item_delegate.h" @@ -229,9 +229,9 @@ void game_list_frame::FixNarrowColumns() void game_list_frame::ResizeColumnsToContents(int spacing) { - if (!m_gameList) - { - return; + if (!m_gameList) + { + return; } m_gameList->verticalHeader()->resizeSections(QHeaderView::ResizeMode::ResizeToContents); @@ -240,9 +240,9 @@ void game_list_frame::ResizeColumnsToContents(int spacing) // Make non-icon columns slighty bigger for better visuals for (int i = 1; i < m_gameList->columnCount(); i++) { - if (m_gameList->isColumnHidden(i)) - { - continue; + if (m_gameList->isColumnHidden(i)) + { + continue; } int size = m_gameList->horizontalHeader()->sectionSize(i) + spacing; @@ -293,42 +293,42 @@ void game_list_frame::SortGameList() int old_row_count = m_gameList->rowCount(); int old_game_count = m_game_data.count(); - for (int i = 0; i < m_gameList->columnCount(); i++) - { - column_widths.append(m_gameList->columnWidth(i)); + for (int i = 0; i < m_gameList->columnCount(); i++) + { + column_widths.append(m_gameList->columnWidth(i)); } // Sorting resizes hidden columns, so unhide them as a workaround QList columns_to_hide; - for (int i = 0; i < m_gameList->columnCount(); i++) - { - if (m_gameList->isColumnHidden(i)) - { - m_gameList->setColumnHidden(i, false); - columns_to_hide << i; - } + for (int i = 0; i < m_gameList->columnCount(); i++) + { + if (m_gameList->isColumnHidden(i)) + { + m_gameList->setColumnHidden(i, false); + columns_to_hide << i; + } } // Sort the list by column and sort order m_gameList->sortByColumn(m_sortColumn, m_colSortOrder); // Hide columns again - for (auto i : columns_to_hide) - { - m_gameList->setColumnHidden(i, true); + for (auto i : columns_to_hide) + { + m_gameList->setColumnHidden(i, true); } // Don't resize the columns if no game is shown to preserve the header settings - if (!m_gameList->rowCount()) - { - for (int i = 0; i < m_gameList->columnCount(); i++) - { - m_gameList->setColumnWidth(i, column_widths[i]); + if (!m_gameList->rowCount()) + { + for (int i = 0; i < m_gameList->columnCount(); i++) + { + m_gameList->setColumnWidth(i, column_widths[i]); } - m_gameList->horizontalHeader()->setSectionResizeMode(gui::column_icon, QHeaderView::Fixed); - return; + m_gameList->horizontalHeader()->setSectionResizeMode(gui::column_icon, QHeaderView::Fixed); + return; } // Fixate vertical header and row height @@ -337,13 +337,13 @@ void game_list_frame::SortGameList() m_gameList->resizeRowsToContents(); // Resize columns if the game list was empty before - if (!old_row_count && !old_game_count) - { - ResizeColumnsToContents(); + if (!old_row_count && !old_game_count) + { + ResizeColumnsToContents(); } - else - { - m_gameList->resizeColumnToContents(gui::column_icon); + else + { + m_gameList->resizeColumnToContents(gui::column_icon); } // Fixate icon column @@ -393,10 +393,8 @@ void game_list_frame::Refresh(const bool fromDrive, const bool scrollAfter) for (const auto& dir : path_list) { try { - const std::string sfb = dir + "/PS3_DISC.SFB"; - const std::string sfo = dir + (fs::is_file(sfb) ? "/PS3_GAME/PARAM.SFO" : "/PARAM.SFO"); - - const fs::file sfo_file(sfo); + const std::string sfo_dir = Emu.GetSfoDirFromGamePath(dir, Emu.GetUsr()); + const fs::file sfo_file(sfo_dir + "/PARAM.SFO"); if (!sfo_file) { continue; @@ -430,29 +428,21 @@ void game_list_frame::Refresh(const bool fromDrive, const bool scrollAfter) auto cat = category::cat_boot.find(game.category); if (cat != category::cat_boot.end()) { - if (game.category == "DG") - { - game.icon_path = dir + "/PS3_GAME/ICON0.PNG"; - } - else - { - game.icon_path = dir + "/ICON0.PNG"; - } - + game.icon_path = sfo_dir + "/ICON0.PNG"; game.category = sstr(cat->second); } else if ((cat = category::cat_data.find(game.category)) != category::cat_data.end()) { - game.icon_path = dir + "/ICON0.PNG"; + game.icon_path = sfo_dir + "/ICON0.PNG"; game.category = sstr(cat->second); } else if (game.category == sstr(category::unknown)) { - game.icon_path = dir + "/ICON0.PNG"; + game.icon_path = sfo_dir + "/ICON0.PNG"; } else { - game.icon_path = dir + "/ICON0.PNG"; + game.icon_path = sfo_dir + "/ICON0.PNG"; game.category = sstr(category::other); } @@ -685,10 +675,10 @@ void game_list_frame::ShowContextMenu(const QPoint &pos) }); connect(removeGame, &QAction::triggered, [=] { - if (currGame.path.empty()) - { - LOG_FATAL(GENERAL, "Cannot remove game. Path is empty"); - return; + if (currGame.path.empty()) + { + LOG_FATAL(GENERAL, "Cannot remove game. Path is empty"); + return; } QMessageBox* mb = new QMessageBox(QMessageBox::Question, tr("Confirm %1 Removal").arg(qstr(currGame.category)), tr("Permanently remove %0 from drive?\nPath: %1").arg(name).arg(qstr(currGame.path)), QMessageBox::Yes | QMessageBox::No, this); @@ -755,14 +745,14 @@ void game_list_frame::ShowContextMenu(const QPoint &pos) connect(editNotes, &QAction::triggered, [=] { bool accepted; - const QString old_notes = m_gui_settings->GetValue(gui::notes, serial, "").toString(); - const QString new_notes = QInputDialog::getMultiLineText(this, tr("Edit Tooltip Notes"), QString("%0\n%1").arg(name).arg(serial), old_notes, &accepted); - - if (accepted) - { - m_notes[serial] = new_notes; - m_gui_settings->SetValue(gui::notes, serial, new_notes); - Refresh(); + const QString old_notes = m_gui_settings->GetValue(gui::notes, serial, "").toString(); + const QString new_notes = QInputDialog::getMultiLineText(this, tr("Edit Tooltip Notes"), QString("%0\n%1").arg(name).arg(serial), old_notes, &accepted); + + if (accepted) + { + m_notes[serial] = new_notes; + m_gui_settings->SetValue(gui::notes, serial, new_notes); + Refresh(); } }); connect(copy_info, &QAction::triggered, [=] @@ -843,18 +833,18 @@ bool game_list_frame::RemoveShadersCache(const std::string& base_dir, bool is_in if (is_interactive && QMessageBox::question(this, tr("Confirm Removal"), tr("Remove shaders cache?")) != QMessageBox::Yes) return false; - QDirIterator dir_iter(qstr(base_dir), QDir::Dirs | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); - while (dir_iter.hasNext()) - { - const QString filepath = dir_iter.next(); - + QDirIterator dir_iter(qstr(base_dir), QDir::Dirs | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); + while (dir_iter.hasNext()) + { + const QString filepath = dir_iter.next(); + if (dir_iter.fileName() == "shaders_cache") { if (QDir(filepath).removeRecursively()) LOG_NOTICE(GENERAL, "Removed shaders cache dir: %s", sstr(filepath)); - else + else LOG_WARNING(GENERAL, "Could not remove shaders cache file: %s", sstr(filepath)); - } + } } LOG_SUCCESS(GENERAL, "Removed shaders cache in %s", base_dir); @@ -869,18 +859,18 @@ bool game_list_frame::RemovePPUCache(const std::string& base_dir, bool is_intera if (is_interactive && QMessageBox::question(this, tr("Confirm Removal"), tr("Remove PPU cache?")) != QMessageBox::Yes) return false; - QDirIterator dir_iter(qstr(base_dir), QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); - while (dir_iter.hasNext()) - { - const QString filepath = dir_iter.next(); - + QDirIterator dir_iter(qstr(base_dir), QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); + while (dir_iter.hasNext()) + { + const QString filepath = dir_iter.next(); + if (dir_iter.fileInfo().absoluteFilePath().endsWith(".obj", Qt::CaseInsensitive)) - { + { if (QFile::remove(filepath)) LOG_NOTICE(GENERAL, "Removed PPU cache file: %s", sstr(filepath)); - else - LOG_WARNING(GENERAL, "Could not remove PPU cache file: %s", sstr(filepath)); - } + else + LOG_WARNING(GENERAL, "Could not remove PPU cache file: %s", sstr(filepath)); + } } LOG_SUCCESS(GENERAL, "Removed PPU cache in %s", base_dir); @@ -895,18 +885,18 @@ bool game_list_frame::RemoveSPUCache(const std::string& base_dir, bool is_intera if (is_interactive && QMessageBox::question(this, tr("Confirm Removal"), tr("Remove SPU cache?")) != QMessageBox::Yes) return false; - QDirIterator dir_iter(qstr(base_dir), QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); - while (dir_iter.hasNext()) - { - const QString filepath = dir_iter.next(); - + QDirIterator dir_iter(qstr(base_dir), QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); + while (dir_iter.hasNext()) + { + const QString filepath = dir_iter.next(); + if (dir_iter.fileInfo().absoluteFilePath().endsWith(".dat", Qt::CaseInsensitive)) { if (QFile::remove(filepath)) LOG_NOTICE(GENERAL, "Removed SPU cache file: %s", sstr(filepath)); - else + else LOG_WARNING(GENERAL, "Could not remove SPU cache file: %s", sstr(filepath)); - } + } } LOG_SUCCESS(GENERAL, "Removed SPU cache in %s", base_dir); @@ -1078,57 +1068,57 @@ bool game_list_frame::eventFilter(QObject *object, QEvent *event) return true; } } - else - { - if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) - { - QTableWidgetItem* item; - - if (object == m_gameList) - item = m_gameList->item(m_gameList->currentRow(), gui::column_icon); - else - item = m_xgrid->currentItem(); - - if (!item || !item->isSelected()) - return false; - + else + { + if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) + { + QTableWidgetItem* item; + + if (object == m_gameList) + item = m_gameList->item(m_gameList->currentRow(), gui::column_icon); + else + item = m_xgrid->currentItem(); + + if (!item || !item->isSelected()) + return false; + game_info gameinfo = GetGameInfoFromItem(item); if (gameinfo.get() == nullptr) - return false; - + return false; + LOG_NOTICE(LOADER, "Booting from gamelist by pressing %s...", keyEvent->key() == Qt::Key_Enter ? "Enter" : "Return"); - Q_EMIT RequestBoot(gameinfo->info.path); - - return true; - } + Q_EMIT RequestBoot(gameinfo->info.path); + + return true; + } } } - else if (event->type() == QEvent::ToolTip) - { - QHelpEvent *helpEvent = static_cast(event); - QTableWidgetItem* item; - - if (m_isListLayout) - { - item = m_gameList->itemAt(helpEvent->globalPos()); - } - else - { - item = m_xgrid->itemAt(helpEvent->globalPos()); - } - - if (item && !item->toolTip().isEmpty() && (!m_isListLayout || item->column() == gui::column_name || item->column() == gui::column_serial)) - { - QToolTip::showText(helpEvent->globalPos(), item->toolTip()); - } - else - { - QToolTip::hideText(); - event->ignore(); - } - - return true; + else if (event->type() == QEvent::ToolTip) + { + QHelpEvent *helpEvent = static_cast(event); + QTableWidgetItem* item; + + if (m_isListLayout) + { + item = m_gameList->itemAt(helpEvent->globalPos()); + } + else + { + item = m_xgrid->itemAt(helpEvent->globalPos()); + } + + if (item && !item->toolTip().isEmpty() && (!m_isListLayout || item->column() == gui::column_name || item->column() == gui::column_serial)) + { + QToolTip::showText(helpEvent->globalPos(), item->toolTip()); + } + else + { + QToolTip::hideText(); + event->ignore(); + } + + return true; } return QDockWidget::eventFilter(object, event); @@ -1174,11 +1164,11 @@ int game_list_frame::PopulateGameList() // Serial custom_table_widget_item* serial_item = new custom_table_widget_item(game->info.serial); - if (!notes.isEmpty()) - { - const QString tool_tip = tr("%0 [%1]\n\nNotes:\n%2").arg(name).arg(serial).arg(notes); - title_item->setToolTip(tool_tip); - serial_item->setToolTip(tool_tip); + if (!notes.isEmpty()) + { + const QString tool_tip = tr("%0 [%1]\n\nNotes:\n%2").arg(name).arg(serial).arg(notes); + title_item->setToolTip(tool_tip); + serial_item->setToolTip(tool_tip); } // Move Support (http://www.psdevwiki.com/ps3/PARAM.SFO#ATTRIBUTE) @@ -1277,13 +1267,13 @@ void game_list_frame::PopulateGameGrid(int maxCols, const QSize& image_size, con m_xgrid->addItem(app->pxmap, title, r, c); m_xgrid->item(r, c)->setData(gui::game_role, QVariant::fromValue(app)); - if (!notes.isEmpty()) - { - m_xgrid->item(r, c)->setToolTip(tr("%0 [%1]\n\nNotes:\n%2").arg(title).arg(serial).arg(notes)); + if (!notes.isEmpty()) + { + m_xgrid->item(r, c)->setToolTip(tr("%0 [%1]\n\nNotes:\n%2").arg(title).arg(serial).arg(notes)); } - else - { - m_xgrid->item(r, c)->setToolTip(tr("%0 [%1]").arg(title).arg(serial)); + else + { + m_xgrid->item(r, c)->setToolTip(tr("%0 [%1]").arg(title).arg(serial)); } if (selected_item == app->info.icon_path) diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index a795d81e06..33e47d079f 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -189,39 +189,35 @@ void main_window::SetAppIconFromPath(const std::string& path) { // get Icon for the gs_frame from path. this handles presumably all possible use cases QString qpath = qstr(path); - std::string icon_list[] = { "/ICON0.PNG", "/PS3_GAME/ICON0.PNG" }; std::string path_list[] = { path, sstr(qpath.section("/", 0, -2)), sstr(qpath.section("/", 0, -3)) }; for (std::string pth : path_list) { if (!fs::is_dir(pth)) continue; - for (std::string ico : icon_list) + const std::string sfo_dir = Emu.GetSfoDirFromGamePath(path, Emu.GetUsr()); + const std::string ico = sfo_dir + "/ICON0.PNG"; + if (fs::is_file(ico)) { - ico = pth + ico; - if (fs::is_file(ico)) - { - // load the image from path. It will most likely be a rectangle - QImage source = QImage(qstr(ico)); - int edgeMax = std::max(source.width(), source.height()); + // load the image from path. It will most likely be a rectangle + QImage source = QImage(qstr(ico)); + int edgeMax = std::max(source.width(), source.height()); - // create a new transparent image with square size and same format as source (maybe handle other formats than RGB32 as well?) - QImage::Format format = source.format() == QImage::Format_RGB32 ? QImage::Format_ARGB32 : source.format(); - QImage dest = QImage(edgeMax, edgeMax, format); - dest.fill(QColor("transparent")); + // create a new transparent image with square size and same format as source (maybe handle other formats than RGB32 as well?) + QImage::Format format = source.format() == QImage::Format_RGB32 ? QImage::Format_ARGB32 : source.format(); + QImage dest = QImage(edgeMax, edgeMax, format); + dest.fill(QColor("transparent")); - // get the location to draw the source image centered within the dest image. - QPoint destPos = source.width() > source.height() ? QPoint(0, (source.width() - source.height()) / 2) - : QPoint((source.height() - source.width()) / 2, 0); + // get the location to draw the source image centered within the dest image. + QPoint destPos = source.width() > source.height() ? QPoint(0, (source.width() - source.height()) / 2) : QPoint((source.height() - source.width()) / 2, 0); - // Paint the source into/over the dest - QPainter painter(&dest); - painter.drawImage(destPos, source); - painter.end(); + // Paint the source into/over the dest + QPainter painter(&dest); + painter.drawImage(destPos, source); + painter.end(); - // set Icon - m_appIcon = QIcon(QPixmap::fromImage(dest)); - return; - } + // set Icon + m_appIcon = QIcon(QPixmap::fromImage(dest)); + return; } } // if nothing was found reset the icon to default @@ -1290,8 +1286,9 @@ void main_window::CreateConnects() connect(ui->actionManage_Users, &QAction::triggered, [=] { - user_manager_dialog* user_manager = new user_manager_dialog(guiSettings, this); - user_manager->show(); + user_manager_dialog user_manager(guiSettings, this); + user_manager.exec(); + m_gameListFrame->Refresh(true); // New user may have different games unlocked. }); connect(ui->toolsCgDisasmAct, &QAction::triggered, [=] @@ -1832,6 +1829,9 @@ void main_window::dropEvent(QDropEvent* event) LOG_SUCCESS(GENERAL, "Successfully copied rap file by drop: %s", rapname); } } + + // Refresh game list since we probably unlocked some games now. + m_gameListFrame->Refresh(true); break; case drop_type::drop_dir: // import valid games to gamelist (games.yaml) for (const auto& path : dropPaths)