diff options
| author | Meow <59313695+a55uka@users.noreply.github.com> | 2026-10-31 11:57:12 +0000 |
|---|---|---|
| committer | Meow <59313695+a55uka@users.noreply.github.com> | 2026-10-31 12:04:06 +0000 |
| commit | 4d2cf418374323b30f7ea59be595fa69fe78a82f (patch) | |
| tree | 794389bade77403559c16488f5a49ed26034d2d9 /src | |
| parent | 2b72e348561bd4696d3e44ca3b855fe565106c64 (diff) | |
game_list, loader: find loose ExeFS dumps and show their name and icon
Diffstat (limited to 'src')
| -rw-r--r-- | src/core/loader/deconstructed_rom_directory.cpp | 18 | ||||
| -rw-r--r-- | src/qt_common/game_list/worker.cpp | 120 |
2 files changed, 98 insertions, 40 deletions
diff --git a/src/core/loader/deconstructed_rom_directory.cpp b/src/core/loader/deconstructed_rom_directory.cpp index 4f2eb1d446..cda1645548 100644 --- a/src/core/loader/deconstructed_rom_directory.cpp +++ b/src/core/loader/deconstructed_rom_directory.cpp @@ -5,6 +5,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include <algorithm> +#include <cctype> #include <cstring> #include <string_view> #include "common/logging.h" @@ -131,9 +132,17 @@ std::string RomFSProductVersion(const FileSys::VirtualDir& romfs_dir) { } // Loose ExeFS dumps carry no control data, but icon/nacp sidecars can be dropped next to the -// ExeFS or in the game folder above it (<game>/exefs/main and +// ExeFS or in the game folder above it (<game>/exefs/main, <game>/base/exefs/main and // <game>/updates/<title id>/exefs/main layouts). std::vector<FileSys::VirtualDir> FindSidecarDirs(const FileSys::VirtualDir& exefs_dir) { + const auto equals_ignore_case = [](const std::string& a, std::string_view b) { + return a.size() == b.size() && + std::equal(a.begin(), a.end(), b.begin(), [](char x, char y) { + return std::tolower(static_cast<unsigned char>(x)) == + std::tolower(static_cast<unsigned char>(y)); + }); + }; + std::vector<FileSys::VirtualDir> dirs{exefs_dir}; for (auto dir = exefs_dir; dirs.size() < 4;) { const auto parent = dir->GetParentDirectory(); @@ -142,10 +151,11 @@ std::vector<FileSys::VirtualDir> FindSidecarDirs(const FileSys::VirtualDir& exef } const auto& name = parent->GetName(); dirs.push_back(parent); - const bool title_id_like = - name == "updates" || + const bool storage_level = + equals_ignore_case(name, "base") || equals_ignore_case(name, "update") || + equals_ignore_case(name, "updates") || (!name.empty() && name.find_first_not_of("0123456789abcdefABCDEF") == std::string::npos); - if (!title_id_like) { + if (!storage_level) { break; } dir = parent; diff --git a/src/qt_common/game_list/worker.cpp b/src/qt_common/game_list/worker.cpp index 9dfc3d7747..14afe2f276 100644 --- a/src/qt_common/game_list/worker.cpp +++ b/src/qt_common/game_list/worker.cpp @@ -168,36 +168,78 @@ QString FormatGameName(const std::string& physical_name) { return physical_name_as_qstring; } -// Loose ExeFS dumps (<game>/exefs/main, updates in <game>/updates[/]/exefs/main) have no control -// data, so they are named after their game folder. +// Loose ExeFS dumps (<game>/exefs/main, <game>/base/exefs/main, updates in +// <game>/updates/<title id>/exefs/main) have no control data, so they are named after their +// game folder. QString LooseExeFSName(const std::string& physical_name) { QDir dir = QFileInfo(QString::fromStdString(physical_name)).dir(); if (dir.dirName().compare(QStringLiteral("exefs"), Qt::CaseInsensitive) != 0 || !dir.cdUp()) { return {}; } - const auto is_updates_dir = [&dir] { - return dir.dirName().compare(QStringLiteral("updates"), Qt::CaseInsensitive) == 0; + // Storage levels sit between the game folder and its content directories. + const auto is_storage_level = [](const QString& name) { + if (name.compare(QStringLiteral("base"), Qt::CaseInsensitive) == 0 || + name.compare(QStringLiteral("update"), Qt::CaseInsensitive) == 0 || + name.compare(QStringLiteral("updates"), Qt::CaseInsensitive) == 0) { + return true; + } + return !name.isEmpty() && + std::all_of(name.cbegin(), name.cend(), [](QChar c) { + return c.isDigit() || (c >= QLatin1Char('a') && c <= QLatin1Char('f')) || + (c >= QLatin1Char('A') && c <= QLatin1Char('F')); + }); }; - // An update may sit in a title-ID-named folder: <game>/updates/<title id>/exefs. - if (!is_updates_dir()) { - const QString tid = dir.dirName(); - if (!std::all_of(tid.cbegin(), tid.cend(), [](QChar c) { return c.isDigit(); }) || - !dir.cdUp() || !is_updates_dir()) { - return tid; + bool is_update = false; + while (is_storage_level(dir.dirName())) { + if (dir.dirName().compare(QStringLiteral("update"), Qt::CaseInsensitive) == 0 || + dir.dirName().compare(QStringLiteral("updates"), Qt::CaseInsensitive) == 0) { + is_update = true; + } + if (!dir.cdUp()) { + return {}; } } - if (!dir.cdUp()) { + const QString name = dir.dirName(); + if (name.isEmpty()) { return {}; } + if (is_update && !name.contains(QStringLiteral("update"), Qt::CaseInsensitive)) { + return QObject::tr("%1 (update)").arg(name); + } + return name; +} - const QString name = dir.dirName(); - if (name.contains(QStringLiteral("update"), Qt::CaseInsensitive)) { - return name; +// A loose dump's game list entry is its small main executable, so the size column would only +// show a few dozen MB. Sum the dump's real contents instead: the ExeFS directory plus the +// RomFS directory next to it, matching the loader's loose RomFS discovery. +u64 LooseDumpSize(const std::filesystem::path& main_path) { + const auto exefs_dir = main_path.parent_path(); + + u64 total{}; + std::error_code ec; + const auto accumulate = [&total, &ec](const std::filesystem::path& dir) { + for (std::filesystem::recursive_directory_iterator it{ + dir, std::filesystem::directory_options::skip_permission_denied, ec}; + !ec && it != std::filesystem::recursive_directory_iterator(); it.increment(ec)) { + std::error_code entry_ec; + if (it->is_regular_file(entry_ec)) { + const auto size = it->file_size(entry_ec); + if (!entry_ec) { + total += size; + } + } + } + }; + + accumulate(exefs_dir); + const auto romfs_dir = exefs_dir.parent_path() / "romfs"; + if (std::filesystem::is_directory(romfs_dir, ec)) { + accumulate(romfs_dir); } - return QObject::tr("%1 (update)").arg(name); + return total; } QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager, @@ -410,7 +452,7 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa std::vector<u64> program_ids; loader->ReadProgramIds(program_ids); - const auto addEntry = [this, physical_name, + const auto addEntry = [this, &path, physical_name, parent_dir](std::unique_ptr<Loader::AppLoader>& app_loader, const u64 id) { std::vector<u8> icon; @@ -428,9 +470,12 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa const FileSys::PatchManager patch{id, system.GetFileSystemController(), system.GetContentProvider()}; - auto entry = MakeGameListEntry( - physical_name, name, Common::FS::GetSize(physical_name), icon, *app_loader, - id, play_time_manager, patch); + const u64 size = IsExtractedNCAMain(physical_name) + ? LooseDumpSize(path) + : Common::FS::GetSize(physical_name); + + auto entry = MakeGameListEntry(physical_name, name, size, icon, *app_loader, id, + play_time_manager, patch); RecordEvent([=](GameListModel* model) { model->AddEntry(entry, parent_dir); }); }; @@ -468,25 +513,28 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa watch_list.append(QString::fromStdString(Common::FS::PathToUTF8String(path))); // Shallow scans stop at the first level, so probe the standard layouts of loose - // ExeFS dumps: <game>/exefs/main and <game>/updates/<title id>/exefs/main. + // ExeFS dumps: <game>/exefs/main, <game>/base/exefs/main and their updates in + // <root>/updates/<title id>/exefs/main. if (!deep_scan && target == ScanTarget::PopulateGameList) { - const auto exefs_main = path / "exefs" / "main"; - if (Common::FS::IsFile(exefs_main)) { - process_candidate(exefs_main); - } + for (const auto& content_root : {path, path / "base"}) { + const auto exefs_main = content_root / "exefs" / "main"; + if (Common::FS::IsFile(exefs_main)) { + process_candidate(exefs_main); + } - const auto updates = path / "updates"; - if (Common::FS::IsDir(updates)) { - Common::FS::IterateDirEntries( - updates, - [&process_candidate](const std::filesystem::path& entry) { - const auto update_main = entry / "exefs" / "main"; - if (Common::FS::IsFile(update_main)) { - process_candidate(update_main); - } - return true; - }, - Common::FS::DirEntryFilter::Directory); + const auto updates = content_root / "updates"; + if (Common::FS::IsDir(updates)) { + Common::FS::IterateDirEntries( + updates, + [&process_candidate](const std::filesystem::path& entry) { + const auto update_main = entry / "exefs" / "main"; + if (Common::FS::IsFile(update_main)) { + process_candidate(update_main); + } + return true; + }, + Common::FS::DirEntryFilter::Directory); + } } } |
