From 7f5de6bcd651f8145e4746c76d0531b6560fd5e4 Mon Sep 17 00:00:00 2001 From: xbzk Date: Wed, 4 Mar 2026 22:51:35 +0100 Subject: [PATCH] [android/fs] external content loader + nca/xci patches (#3596) Foreword: WHY DON'T EVERYBODY USE ONE FOLDER FOR EACH GAME+CONTENTS? AIN'T THIS THE FORMAT GAMES COME WHEN YOU BUE THEM? DO YOU LIVE WITH ALL YOUR FRIENDS AND HAVE A 2ND HOUSE FOR ALL THE CHILDREN? Nice, i feel better now. This feat extends Maufeat's work on external content loading. It harmonically additions: "...also, if in each game folder X, you find a folder Y, and in this folder Y you detect ONLY a single game, then mount all external content for that game found in that folder Y and its subfolders." Permanent (not toggleable). External Content folders are supported equally. Also: -Reworked several routines for preserving single source of truth between android and other systems; -Fixed the annoying unknown format error for content files, by providing proper format detection. Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3596 Reviewed-by: MaranBr Reviewed-by: Lizzie Co-authored-by: xbzk Co-committed-by: xbzk --- .../java/org/yuzu/yuzu_emu/NativeLibrary.kt | 6 + .../org/yuzu/yuzu_emu/model/AddonViewModel.kt | 6 +- .../org/yuzu/yuzu_emu/ui/main/MainActivity.kt | 4 +- .../org/yuzu/yuzu_emu/utils/GameHelper.kt | 64 ++- .../app/src/main/jni/android_config.cpp | 6 + .../app/src/main/jni/game_metadata.cpp | 5 + src/android/app/src/main/jni/native.cpp | 116 +---- src/android/app/src/main/jni/native.h | 4 + src/common/settings.h | 2 + src/core/file_sys/patch_manager.cpp | 27 +- src/core/file_sys/registered_cache.cpp | 473 +++++++++--------- src/core/file_sys/registered_cache.h | 5 +- src/core/loader/loader.cpp | 70 ++- src/core/loader/loader.h | 22 +- src/core/loader/nsp.cpp | 36 +- src/core/loader/xci.cpp | 14 +- src/yuzu/game/game_list_worker.cpp | 22 +- src/yuzu/main_window.cpp | 19 +- 18 files changed, 477 insertions(+), 424 deletions(-) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index 1f0acf2835..397b44c9f9 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt @@ -607,6 +607,12 @@ object NativeLibrary { */ external fun addFileToFilesystemProvider(path: String) + /** + * Adds a game-folder file to the manual filesystem provider, respecting the internal gate for + * game-folder external-content mounting. + */ + external fun addGameFolderFileToFilesystemProvider(path: String) + /** * Clears all files added to the manual filesystem provider in our EmulationSession instance */ diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt index c682a13cfc..2a0e72be26 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt @@ -127,10 +127,6 @@ class AddonViewModel : ViewModel() { return } - // Check if there are multiple update versions - val updates = _patchList.value.filter { PatchType.from(it.type) == PatchType.Update } - val hasMultipleUpdates = updates.size > 1 - NativeConfig.setDisabledAddons( game!!.programId, _patchList.value.mapNotNull { @@ -140,7 +136,7 @@ class AddonViewModel : ViewModel() { if (PatchType.from(it.type) == PatchType.Update) { if (it.name.contains("(NAND)") || it.name.contains("(SDMC)")) { it.name - } else if (hasMultipleUpdates) { + } else if (it.numericVersion != 0L) { "Update@${it.numericVersion}" } else { it.name diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index 8a4262ebe7..db4cc0f60e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -424,7 +424,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider { ) val uriString = result.toString() - val folder = gamesViewModel.folders.value.firstOrNull { it.uriString == uriString } + val folder = gamesViewModel.folders.value.firstOrNull { + it.uriString == uriString && it.type == org.yuzu.yuzu_emu.model.DirectoryType.EXTERNAL_CONTENT + } if (folder != null) { Toast.makeText( applicationContext, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt index fff5fdfb9b..4a3cf61daa 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt @@ -51,11 +51,24 @@ object GameHelper { // Scan External Content directories and register all NSP/XCI files val externalContentDirs = NativeConfig.getExternalContentDirs() - for (externalDir in externalContentDirs) { + val uniqueExternalContentDirs = linkedSetOf() + externalContentDirs.forEach { externalDir -> + if (externalDir.isNotEmpty()) { + uniqueExternalContentDirs.add(externalDir) + } + } + + val mountedContainerUris = mutableSetOf() + for (externalDir in uniqueExternalContentDirs) { if (externalDir.isNotEmpty()) { val externalDirUri = externalDir.toUri() if (FileUtil.isTreeUriValid(externalDirUri)) { - scanExternalContentRecursive(FileUtil.listFiles(externalDirUri), 3) + scanContentContainersRecursive(FileUtil.listFiles(externalDirUri), 3) { + val containerUri = it.uri.toString() + if (mountedContainerUris.add(containerUri)) { + NativeLibrary.addFileToFilesystemProvider(containerUri) + } + } } } } @@ -65,10 +78,13 @@ object GameHelper { val gameDirUri = gameDir.uriString.toUri() val isValid = FileUtil.isTreeUriValid(gameDirUri) if (isValid) { + val scanDepth = if (gameDir.deepScan) 3 else 1 + addGamesRecursive( games, FileUtil.listFiles(gameDirUri), - if (gameDir.deepScan) 3 else 1 + scanDepth, + mountedContainerUris ) } else { badDirs.add(index) @@ -103,9 +119,10 @@ object GameHelper { // be done better imo. private val externalContentExtensions = setOf("nsp", "xci") - private fun scanExternalContentRecursive( + private fun scanContentContainersRecursive( files: Array, - depth: Int + depth: Int, + onContainerFound: (MinimalDocumentFile) -> Unit ) { if (depth <= 0) { return @@ -113,14 +130,15 @@ object GameHelper { files.forEach { if (it.isDirectory) { - scanExternalContentRecursive( + scanContentContainersRecursive( FileUtil.listFiles(it.uri), - depth - 1 + depth - 1, + onContainerFound ) } else { val extension = FileUtil.getExtension(it.uri).lowercase() if (externalContentExtensions.contains(extension)) { - NativeLibrary.addFileToFilesystemProvider(it.uri.toString()) + onContainerFound(it) } } } @@ -129,7 +147,8 @@ object GameHelper { private fun addGamesRecursive( games: MutableList, files: Array, - depth: Int + depth: Int, + mountedContainerUris: MutableSet ) { if (depth <= 0) { return @@ -140,11 +159,20 @@ object GameHelper { addGamesRecursive( games, FileUtil.listFiles(it.uri), - depth - 1 + depth - 1, + mountedContainerUris ) } else { - if (Game.extensions.contains(FileUtil.getExtension(it.uri))) { - val game = getGame(it.uri, true) + val extension = FileUtil.getExtension(it.uri).lowercase() + val filePath = it.uri.toString() + + if (externalContentExtensions.contains(extension) && + mountedContainerUris.add(filePath)) { + NativeLibrary.addGameFolderFileToFilesystemProvider(filePath) + } + + if (Game.extensions.contains(extension)) { + val game = getGame(it.uri, true, false) if (game != null) { games.add(game) } @@ -153,14 +181,20 @@ object GameHelper { } } - fun getGame(uri: Uri, addedToLibrary: Boolean): Game? { + fun getGame( + uri: Uri, + addedToLibrary: Boolean, + registerFilesystemProvider: Boolean = true + ): Game? { val filePath = uri.toString() if (!GameMetadata.getIsValid(filePath)) { return null } - // Needed to update installed content information - NativeLibrary.addFileToFilesystemProvider(filePath) + if (registerFilesystemProvider) { + // Needed to update installed content information + NativeLibrary.addFileToFilesystemProvider(filePath) + } var name = GameMetadata.getTitle(filePath) diff --git a/src/android/app/src/main/jni/android_config.cpp b/src/android/app/src/main/jni/android_config.cpp index 0171e2a7b3..0c5696ef3f 100644 --- a/src/android/app/src/main/jni/android_config.cpp +++ b/src/android/app/src/main/jni/android_config.cpp @@ -33,6 +33,12 @@ void AndroidConfig::ReadAndroidValues() { if (global) { ReadAndroidUIValues(); ReadUIValues(); + BeginGroup(Settings::TranslateCategory(Settings::Category::DataStorage)); + Settings::values.ext_content_from_game_dirs = ReadBooleanSetting( + std::string("ext_content_from_game_dirs"), + std::make_optional( + Settings::values.ext_content_from_game_dirs.GetDefault())); + EndGroup(); ReadOverlayValues(); } ReadDriverValues(); diff --git a/src/android/app/src/main/jni/game_metadata.cpp b/src/android/app/src/main/jni/game_metadata.cpp index e9c03b6440..9acba456f1 100644 --- a/src/android/app/src/main/jni/game_metadata.cpp +++ b/src/android/app/src/main/jni/game_metadata.cpp @@ -96,6 +96,11 @@ jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsValid(JNIEnv* env, jobj return false; } + if ((file_type == Loader::FileType::NSP || file_type == Loader::FileType::XCI) && + !Loader::IsBootableGameContainer(file, file_type)) { + return false; + } + u64 program_id = 0; Loader::ResultStatus res = loader->ReadProgramId(program_id); if (res != Loader::ResultStatus::Success) { diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index c429f4a1e4..3f0029c78a 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -217,107 +217,8 @@ void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath) return; } - const auto extension = Common::ToLower(filepath.substr(filepath.find_last_of('.') + 1)); - - if (extension == "nsp") { - auto nsp = std::make_shared(file); - if (nsp->GetStatus() == Loader::ResultStatus::Success) { - std::map nsp_versions; - std::map nsp_version_strings; - - for (const auto& [title_id, nca_map] : nsp->GetNCAs()) { - for (const auto& [type_pair, nca] : nca_map) { - const auto& [title_type, content_type] = type_pair; - - if (content_type == FileSys::ContentRecordType::Meta) { - const auto meta_nca = std::make_shared(nca->GetBaseFile()); - if (meta_nca->GetStatus() == Loader::ResultStatus::Success) { - const auto section0 = meta_nca->GetSubdirectories(); - if (!section0.empty()) { - for (const auto& meta_file : section0[0]->GetFiles()) { - if (meta_file->GetExtension() == "cnmt") { - FileSys::CNMT cnmt(meta_file); - nsp_versions[cnmt.GetTitleID()] = cnmt.GetTitleVersion(); - } - } - } - } - } - - if (content_type == FileSys::ContentRecordType::Control && - title_type == FileSys::TitleType::Update) { - auto romfs = nca->GetRomFS(); - if (romfs) { - auto extracted = FileSys::ExtractRomFS(romfs); - if (extracted) { - auto nacp_file = extracted->GetFile("control.nacp"); - if (!nacp_file) { - nacp_file = extracted->GetFile("Control.nacp"); - } - if (nacp_file) { - FileSys::NACP nacp(nacp_file); - auto ver_str = nacp.GetVersionString(); - if (!ver_str.empty()) { - nsp_version_strings[title_id] = ver_str; - } - } - } - } - } - } - } - - for (const auto& [title_id, nca_map] : nsp->GetNCAs()) { - for (const auto& [type_pair, nca] : nca_map) { - const auto& [title_type, content_type] = type_pair; - - if (title_type == FileSys::TitleType::Update) { - u32 version = 0; - auto ver_it = nsp_versions.find(title_id); - if (ver_it != nsp_versions.end()) { - version = ver_it->second; - } - - std::string version_string; - auto str_it = nsp_version_strings.find(title_id); - if (str_it != nsp_version_strings.end()) { - version_string = str_it->second; - } - - m_manual_provider->AddEntryWithVersion( - title_type, content_type, title_id, version, version_string, - nca->GetBaseFile()); - - LOG_DEBUG(Frontend, "Added NSP update entry - TitleID: {:016X}, Version: {}, VersionStr: {}", - title_id, version, version_string); - } else { - // Use regular AddEntry for non-updates - m_manual_provider->AddEntry(title_type, content_type, title_id, - nca->GetBaseFile()); - LOG_DEBUG(Frontend, "Added NSP entry - TitleID: {:016X}, TitleType: {}, ContentType: {}", - title_id, static_cast(title_type), static_cast(content_type)); - } - } - } - return; - } - } - - // Handle XCI files - if (extension == "xci") { - FileSys::XCI xci{file}; - if (xci.GetStatus() == Loader::ResultStatus::Success) { - const auto nsp = xci.GetSecurePartitionNSP(); - if (nsp) { - for (const auto& title : nsp->GetNCAs()) { - for (const auto& entry : title.second) { - m_manual_provider->AddEntry(entry.first.first, entry.first.second, title.first, - entry.second->GetBaseFile()); - } - } - } - return; - } + if (m_manual_provider->AddEntriesFromContainer(file)) { + return; } auto loader = Loader::GetLoader(m_system, file); @@ -339,6 +240,13 @@ void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath) } } +void EmulationSession::ConfigureFilesystemProviderFromGameFolder(const std::string& filepath) { + if (!Settings::values.ext_content_from_game_dirs.GetValue()) { + return; + } + ConfigureFilesystemProvider(filepath); +} + void EmulationSession::InitializeSystem(bool reload) { if (!reload) { // Initialize logging system @@ -1609,6 +1517,12 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_addFileToFilesystemProvider(JNIEnv* e Common::Android::GetJString(env, jpath)); } +void Java_org_yuzu_yuzu_1emu_NativeLibrary_addGameFolderFileToFilesystemProvider( + JNIEnv* env, jobject jobj, jstring jpath) { + EmulationSession::GetInstance().ConfigureFilesystemProviderFromGameFolder( + Common::Android::GetJString(env, jpath)); +} + void Java_org_yuzu_yuzu_1emu_NativeLibrary_clearFilesystemProvider(JNIEnv* env, jobject jobj) { EmulationSession::GetInstance().GetContentProvider()->ClearAllEntries(); } diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h index dfbc8b2943..f2e5c2cfd6 100644 --- a/src/android/app/src/main/jni/native.h +++ b/src/android/app/src/main/jni/native.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -46,6 +49,7 @@ public: const Core::PerfStatsResults& PerfStats(); int ShadersBuilding(); void ConfigureFilesystemProvider(const std::string& filepath); + void ConfigureFilesystemProviderFromGameFolder(const std::string& filepath); void InitializeSystem(bool reload); void SetAppletId(int applet_id); Core::SystemResultStatus InitializeEmulation(const std::string& filepath, diff --git a/src/common/settings.h b/src/common/settings.h index 7ea4136576..7c6c0d062f 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -756,6 +756,8 @@ struct Values { Category::DataStorage}; Setting gamecard_path{linkage, std::string(), "gamecard_path", Category::DataStorage}; + Setting ext_content_from_game_dirs{linkage, true, "ext_content_from_game_dirs", + Category::DataStorage}; std::vector external_content_dirs; // Debugging diff --git a/src/core/file_sys/patch_manager.cpp b/src/core/file_sys/patch_manager.cpp index 82944ceceb..e9c3bb75c2 100644 --- a/src/core/file_sys/patch_manager.cpp +++ b/src/core/file_sys/patch_manager.cpp @@ -117,6 +117,12 @@ void AppendCommaIfNotEmpty(std::string& to, std::string_view with) { bool IsDirValidAndNonEmpty(const VirtualDir& dir) { return dir != nullptr && (!dir->GetFiles().empty() || !dir->GetSubdirectories().empty()); } + +bool IsVersionedExternalUpdateDisabled(const std::vector& disabled, u32 version) { + const std::string disabled_key = fmt::format("Update@{}", version); + return std::find(disabled.cbegin(), disabled.cend(), disabled_key) != disabled.cend() || + std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); +} } // Anonymous namespace PatchManager::PatchManager(u64 title_id_, @@ -155,8 +161,7 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const { if (!update_versions.empty()) { checked_external = true; for (const auto& update_entry : update_versions) { - std::string disabled_key = fmt::format("Update@{}", update_entry.version); - if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) { + if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) { update_disabled = false; enabled_version = update_entry.version; break; @@ -175,8 +180,7 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const { if (!manual_update_versions.empty()) { checked_manual = true; for (const auto& update_entry : manual_update_versions) { - std::string disabled_key = fmt::format("Update@{}", update_entry.version); - if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) { + if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) { update_disabled = false; enabled_version = update_entry.version; break; @@ -580,8 +584,7 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs if (!update_versions.empty()) { checked_external = true; for (const auto& update_entry : update_versions) { - std::string disabled_key = fmt::format("Update@{}", update_entry.version); - if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) { + if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) { update_disabled = false; enabled_version = update_entry.version; update_raw = external_provider->GetEntryForVersion(update_tid, type, update_entry.version); @@ -600,8 +603,7 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs if (!manual_update_versions.empty()) { checked_manual = true; for (const auto& update_entry : manual_update_versions) { - std::string disabled_key = fmt::format("Update@{}", update_entry.version); - if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) { + if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) { update_disabled = false; enabled_version = update_entry.version; update_raw = manual_provider->GetEntryForVersion(update_tid, type, update_entry.version); @@ -704,9 +706,8 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { version_str = FormatTitleVersion(update_entry.version); } - std::string disabled_key = fmt::format("Update@{}", update_entry.version); const auto update_disabled = - std::find(disabled.cbegin(), disabled.cend(), disabled_key) != disabled.cend(); + IsVersionedExternalUpdateDisabled(disabled, update_entry.version); Patch update_patch = {.enabled = !update_disabled, .name = "Update", @@ -732,9 +733,8 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { version_str = FormatTitleVersion(update_entry.version); } - std::string disabled_key = fmt::format("Update@{}", update_entry.version); const auto update_disabled = - std::find(disabled.cbegin(), disabled.cend(), disabled_key) != disabled.cend(); + IsVersionedExternalUpdateDisabled(disabled, update_entry.version); Patch update_patch = {.enabled = !update_disabled, @@ -771,7 +771,8 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { std::nullopt, std::nullopt, ContentRecordType::Program, update_tid); for (const auto& [slot, entry] : all_updates) { - if (slot == ContentProviderUnionSlot::External) { + if (slot == ContentProviderUnionSlot::External || + slot == ContentProviderUnionSlot::FrontendManual) { continue; } diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp index 7bf2ad8fcd..020d403c95 100644 --- a/src/core/file_sys/registered_cache.cpp +++ b/src/core/file_sys/registered_cache.cpp @@ -104,6 +104,206 @@ static std::string GetCNMTName(TitleType type, u64 title_id) { return fmt::format("{}_{:016x}.cnmt", TITLE_TYPE_NAMES[index], title_id); } +static std::shared_ptr OpenContainerAsNsp(const VirtualFile& file, Loader::FileType type) { + if (!file) { + return nullptr; + } + + if (type == Loader::FileType::Unknown || type == Loader::FileType::Error) { + type = Loader::IdentifyFile(file); + if (type == Loader::FileType::Unknown) { + type = Loader::GuessFromFilename(file->GetName()); + } + } + + if (type == Loader::FileType::NSP) { + auto nsp = std::make_shared(file); + return nsp->GetStatus() == Loader::ResultStatus::Success ? nsp : nullptr; + } + + if (type == Loader::FileType::XCI) { + XCI xci(file); + if (xci.GetStatus() != Loader::ResultStatus::Success) { + return nullptr; + } + + auto secure_partition = xci.GetSecurePartitionNSP(); + if (secure_partition == nullptr) { + return nullptr; + } + + return secure_partition; + } + + // SAF-backed files can occasionally fail type-guessing despite being valid NSP/XCI. + // As a last resort, probe both container parsers directly. + { + auto nsp = std::make_shared(file); + if (nsp->GetStatus() == Loader::ResultStatus::Success) { + return nsp; + } + } + { + XCI xci(file); + if (xci.GetStatus() == Loader::ResultStatus::Success) { + auto secure_partition = xci.GetSecurePartitionNSP(); + if (secure_partition != nullptr) { + return secure_partition; + } + } + } + + return nullptr; +} + +template +bool ForEachContainerEntry(const std::shared_ptr& nsp, bool only_content, + std::optional base_program_id, Callback&& on_entry) { + if (!nsp) { + return false; + } + + const auto& ncas = nsp->GetNCAs(); + if (ncas.empty()) { + return false; + } + + std::map versions; + std::map version_strings; + + for (const auto& [title_id, nca_map] : ncas) { + for (const auto& [type_pair, nca] : nca_map) { + if (!nca) { + continue; + } + + const auto& [title_type, content_type] = type_pair; + + if (content_type == ContentRecordType::Meta) { + const auto subdirs = nca->GetSubdirectories(); + if (!subdirs.empty()) { + for (const auto& inner_file : subdirs[0]->GetFiles()) { + if (inner_file->GetExtension() == "cnmt") { + const CNMT cnmt(inner_file); + versions[cnmt.GetTitleID()] = cnmt.GetTitleVersion(); + break; + } + } + } + } + + if (title_type == TitleType::Update && content_type == ContentRecordType::Control) { + const auto romfs = nca->GetRomFS(); + if (!romfs) { + continue; + } + + const auto extracted = ExtractRomFS(romfs); + if (!extracted) { + continue; + } + + auto nacp_file = extracted->GetFile("control.nacp"); + if (!nacp_file) { + nacp_file = extracted->GetFile("Control.nacp"); + } + if (!nacp_file) { + continue; + } + + const NACP nacp(nacp_file); + auto version_string = nacp.GetVersionString(); + if (!version_string.empty()) { + version_strings[title_id] = std::move(version_string); + } + } + } + } + + bool added_entries = false; + for (const auto& [title_id, nca_map] : ncas) { + if (base_program_id.has_value() && GetBaseTitleID(title_id) != *base_program_id) { + continue; + } + + for (const auto& [type_pair, nca] : nca_map) { + const auto& [title_type, content_type] = type_pair; + if (only_content && title_type != TitleType::Update && title_type != TitleType::AOC) { + continue; + } + + auto entry_file = nca ? nca->GetBaseFile() : nullptr; + if (!entry_file) { + continue; + } + + u32 version = 0; + std::string version_string; + + if (title_type == TitleType::Update) { + if (const auto version_it = versions.find(title_id); version_it != versions.end()) { + version = version_it->second; + } + + if (const auto version_str_it = version_strings.find(title_id); + version_str_it != version_strings.end()) { + version_string = version_str_it->second; + } + } + + on_entry(title_type, content_type, title_id, entry_file, version, version_string); + added_entries = true; + } + } + + return added_entries; +} + +static void UpsertExternalVersionEntry(std::vector& multi_version_entries, + u64 title_id, u32 version, + const std::string& version_string, + ContentRecordType content_type, const VirtualFile& file) { + auto it = std::find_if(multi_version_entries.begin(), multi_version_entries.end(), + [title_id, version](const ExternalUpdateEntry& entry) { + return entry.title_id == title_id && entry.version == version; + }); + + if (it == multi_version_entries.end()) { + ExternalUpdateEntry update_entry; + update_entry.title_id = title_id; + update_entry.version = version; + update_entry.version_string = version_string; + update_entry.files[static_cast(content_type)] = file; + multi_version_entries.push_back(std::move(update_entry)); + return; + } + + it->files[static_cast(content_type)] = file; + if (it->version_string.empty() && !version_string.empty()) { + it->version_string = version_string; + } +} + +template +static bool AddExternalEntriesFromContainer(const std::shared_ptr& nsp, EntryMap& entries, + VersionMap& versions, + std::vector& multi_version_entries) { + return ForEachContainerEntry( + nsp, true, std::nullopt, + [&entries, &versions, + &multi_version_entries](TitleType title_type, ContentRecordType content_type, u64 title_id, + const VirtualFile& file, u32 version, + const std::string& version_string) { + entries[{title_id, content_type, title_type}] = file; + + if (title_type == TitleType::Update) { + versions[title_id] = version; + UpsertExternalVersionEntry(multi_version_entries, title_id, version, version_string, + content_type, file); + } + }); +} + ContentRecordType GetCRTypeFromNCAType(NCAContentType type) { switch (type) { case NCAContentType::Program: @@ -1008,6 +1208,26 @@ void ManualContentProvider::AddEntryWithVersion(TitleType title_type, ContentRec } } +bool ManualContentProvider::AddEntriesFromContainer(VirtualFile file, bool only_content, + std::optional base_program_id) { + const auto nsp = OpenContainerAsNsp(file, Loader::FileType::Unknown); + if (!nsp) { + return false; + } + + return ForEachContainerEntry( + nsp, only_content, base_program_id, + [this](TitleType title_type, ContentRecordType content_type, u64 title_id, + const VirtualFile& entry_file, u32 version, const std::string& version_string) { + if (title_type == TitleType::Update) { + AddEntryWithVersion(title_type, content_type, title_id, version, version_string, + entry_file); + } else { + AddEntry(title_type, content_type, title_id, entry_file); + } + }); +} + void ManualContentProvider::ClearAllEntries() { entries.clear(); multi_version_entries.clear(); @@ -1091,14 +1311,6 @@ VirtualFile ManualContentProvider::GetEntryForVersion(u64 title_id, ContentRecor return nullptr; } -bool ManualContentProvider::HasMultipleVersions(u64 title_id, ContentRecordType type) const { - size_t count = 0; - for (const auto& entry : multi_version_entries) - if (entry.title_id == title_id && entry.files[size_t(type)]) - ++count; - return count > 0; -} - ExternalContentProvider::ExternalContentProvider(std::vector load_directories) : load_dirs(std::move(load_directories)) { ExternalContentProvider::Refresh(); @@ -1159,247 +1371,22 @@ void ExternalContentProvider::ScanDirectory(const VirtualDir& dir) { } void ExternalContentProvider::ProcessNSP(const VirtualFile& file) { - auto nsp = NSP(file); - if (nsp.GetStatus() != Loader::ResultStatus::Success) { + const auto nsp = OpenContainerAsNsp(file, Loader::FileType::NSP); + if (!nsp) { return; } LOG_DEBUG(Service_FS, "Processing NSP file: {}", file->GetName()); - - const auto ncas = nsp.GetNCAs(); - - std::map nsp_versions; - std::map nsp_version_strings; // title_id -> NACP version string - - for (const auto& [title_id, nca_map] : ncas) { - for (const auto& [type_pair, nca] : nca_map) { - const auto& [title_type, content_type] = type_pair; - - if (content_type == ContentRecordType::Meta) { - const auto subdirs = nca->GetSubdirectories(); - if (!subdirs.empty()) { - const auto section0 = subdirs[0]; - const auto files = section0->GetFiles(); - for (const auto& inner_file : files) { - if (inner_file->GetExtension() == "cnmt") { - const CNMT cnmt(inner_file); - const auto cnmt_title_id = cnmt.GetTitleID(); - const auto version = cnmt.GetTitleVersion(); - nsp_versions[cnmt_title_id] = version; - versions[cnmt_title_id] = version; - break; - } - } - } - } - - if (content_type == ContentRecordType::Control && title_type == TitleType::Update) { - auto romfs = nca->GetRomFS(); - if (romfs) { - auto extracted = ExtractRomFS(romfs); - if (extracted) { - auto nacp_file = extracted->GetFile("control.nacp"); - if (!nacp_file) { - nacp_file = extracted->GetFile("Control.nacp"); - } - if (nacp_file) { - NACP nacp(nacp_file); - auto ver_str = nacp.GetVersionString(); - if (!ver_str.empty()) { - nsp_version_strings[title_id] = ver_str; - } - } - } - } - } - } - } - - std::map, std::array> version_files; - - for (const auto& [title_id, nca_map] : ncas) { - for (const auto& [type_pair, nca] : nca_map) { - const auto& [title_type, content_type] = type_pair; - - if (title_type != TitleType::AOC && title_type != TitleType::Update) { - continue; - } - - auto nca_file = nsp.GetNCAFile(title_id, content_type, title_type); - if (nca_file != nullptr) { - entries[{title_id, content_type, title_type}] = nca_file; - - if (title_type == TitleType::Update) { - u32 version = 0; - auto ver_it = nsp_versions.find(title_id); - if (ver_it != nsp_versions.end()) { - version = ver_it->second; - } - - version_files[{title_id, version}][size_t(content_type)] = nca_file; - } - - LOG_DEBUG(Service_FS, "Added entry - Title ID: {:016X}, Type: {}, Content: {}", - title_id, static_cast(title_type), static_cast(content_type)); - } - } - } - - for (const auto& [key, files_map] : version_files) { - const auto& [title_id, version] = key; - - std::string ver_str; - auto str_it = nsp_version_strings.find(title_id); - if (str_it != nsp_version_strings.end()) { - ver_str = str_it->second; - } - - bool version_exists = false; - for (auto& existing : multi_version_entries) { - if (existing.title_id == title_id && existing.version == version) { - existing.files = files_map; - if (existing.version_string.empty() && !ver_str.empty()) { - existing.version_string = ver_str; - } - version_exists = true; - break; - } - } - - if (!version_exists && !files_map.empty()) { - ExternalUpdateEntry update_entry{ - .title_id = title_id, - .version = version, - .version_string = ver_str, - .files = files_map - }; - multi_version_entries.push_back(update_entry); - LOG_DEBUG(Service_FS, "Added multi-version update - Title ID: {:016X}, Version: {}, VersionStr: {}, Content types: {}", - title_id, version, ver_str, files_map.size()); - } - } + AddExternalEntriesFromContainer(nsp, entries, versions, multi_version_entries); } void ExternalContentProvider::ProcessXCI(const VirtualFile& file) { - auto xci = XCI(file); - if (xci.GetStatus() != Loader::ResultStatus::Success) { - return; - } - - auto nsp = xci.GetSecurePartitionNSP(); - if (nsp == nullptr) { + const auto nsp = OpenContainerAsNsp(file, Loader::FileType::XCI); + if (!nsp) { return; } - const auto ncas = nsp->GetNCAs(); - - std::map xci_versions; - std::map xci_version_strings; - - for (const auto& [title_id, nca_map] : ncas) { - for (const auto& [type_pair, nca] : nca_map) { - const auto& [title_type, content_type] = type_pair; - - if (content_type == ContentRecordType::Meta) { - const auto subdirs = nca->GetSubdirectories(); - if (!subdirs.empty()) { - const auto section0 = subdirs[0]; - const auto files = section0->GetFiles(); - for (const auto& inner_file : files) { - if (inner_file->GetExtension() == "cnmt") { - const CNMT cnmt(inner_file); - const auto cnmt_title_id = cnmt.GetTitleID(); - const auto version = cnmt.GetTitleVersion(); - xci_versions[cnmt_title_id] = version; - versions[cnmt_title_id] = version; - break; - } - } - } - } - - if (content_type == ContentRecordType::Control && title_type == TitleType::Update) { - auto romfs = nca->GetRomFS(); - if (romfs) { - auto extracted = ExtractRomFS(romfs); - if (extracted) { - auto nacp_file = extracted->GetFile("control.nacp"); - if (!nacp_file) { - nacp_file = extracted->GetFile("Control.nacp"); - } - if (nacp_file) { - NACP nacp(nacp_file); - auto ver_str = nacp.GetVersionString(); - if (!ver_str.empty()) { - xci_version_strings[title_id] = ver_str; - } - } - } - } - } - } - } - - std::map, std::array> version_files; - - for (const auto& [title_id, nca_map] : ncas) { - for (const auto& [type_pair, nca] : nca_map) { - const auto& [title_type, content_type] = type_pair; - - if (title_type != TitleType::AOC && title_type != TitleType::Update) { - continue; - } - - auto nca_file = nsp->GetNCAFile(title_id, content_type, title_type); - if (nca_file != nullptr) { - entries[{title_id, content_type, title_type}] = nca_file; - - if (title_type == TitleType::Update) { - u32 version = 0; - auto ver_it = xci_versions.find(title_id); - if (ver_it != xci_versions.end()) { - version = ver_it->second; - } - - version_files[{title_id, version}][size_t(content_type)] = nca_file; - } - } - } - } - - for (const auto& [key, files_map] : version_files) { - const auto& [title_id, version] = key; - - std::string ver_str; - auto str_it = xci_version_strings.find(title_id); - if (str_it != xci_version_strings.end()) { - ver_str = str_it->second; - } - - bool version_exists = false; - for (auto& existing : multi_version_entries) { - if (existing.title_id == title_id && existing.version == version) { - existing.files = files_map; - if (existing.version_string.empty() && !ver_str.empty()) { - existing.version_string = ver_str; - } - version_exists = true; - break; - } - } - - if (!version_exists && !files_map.empty()) { - ExternalUpdateEntry update_entry{ - .title_id = title_id, - .version = version, - .version_string = ver_str, - .files = files_map - }; - multi_version_entries.push_back(update_entry); - LOG_DEBUG(Service_FS, "Added multi-version update from XCI - Title ID: {:016X}, Version: {}, VersionStr: {}, Content types: {}", - title_id, version, ver_str, files_map.size()); - } - } + AddExternalEntriesFromContainer(nsp, entries, versions, multi_version_entries); } bool ExternalContentProvider::HasEntry(u64 title_id, ContentRecordType type) const { @@ -1491,12 +1478,4 @@ VirtualFile ExternalContentProvider::GetEntryForVersion(u64 title_id, ContentRec return nullptr; } -bool ExternalContentProvider::HasMultipleVersions(u64 title_id, ContentRecordType type) const { - size_t count = 0; - for (const auto& entry : multi_version_entries) - if (entry.title_id == title_id && entry.files[size_t(type)]) - ++count; - return count > 1; -} - } // namespace FileSys diff --git a/src/core/file_sys/registered_cache.h b/src/core/file_sys/registered_cache.h index 2e39f74db8..32134d1c48 100644 --- a/src/core/file_sys/registered_cache.h +++ b/src/core/file_sys/registered_cache.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -262,6 +263,8 @@ public: VirtualFile file); void AddEntryWithVersion(TitleType title_type, ContentRecordType content_type, u64 title_id, u32 version, const std::string& version_string, VirtualFile file); + bool AddEntriesFromContainer(VirtualFile file, bool only_content = false, + std::optional base_program_id = std::nullopt); void ClearAllEntries(); void Refresh() override; @@ -276,7 +279,6 @@ public: std::vector ListUpdateVersions(u64 title_id) const; VirtualFile GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const; - bool HasMultipleVersions(u64 title_id, ContentRecordType type) const; private: std::map, VirtualFile> entries; @@ -303,7 +305,6 @@ public: std::vector ListUpdateVersions(u64 title_id) const; VirtualFile GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const; - bool HasMultipleVersions(u64 title_id, ContentRecordType type) const; private: void ScanDirectory(const VirtualDir& dir); diff --git a/src/core/loader/loader.cpp b/src/core/loader/loader.cpp index 4379634d03..b4d50227d3 100644 --- a/src/core/loader/loader.cpp +++ b/src/core/loader/loader.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project @@ -9,11 +9,15 @@ #include #include #include +#include #include "common/concepts.h" #include "common/fs/path_util.h" #include "common/logging/log.h" #include "common/string_util.h" #include "core/core.h" +#include "core/file_sys/card_image.h" +#include "core/file_sys/common_funcs.h" +#include "core/file_sys/submission_package.h" #include "core/hle/kernel/k_process.h" #include "core/loader/deconstructed_rom_directory.h" #include "core/loader/kip.h" @@ -37,6 +41,49 @@ std::optional IdentifyFileLoader(FileSys::VirtualFile file) { return std::nullopt; } +std::shared_ptr OpenContainerAsNsp(FileSys::VirtualFile file, FileType type, + u64 program_id = 0, + std::size_t program_index = 0) { + if (!file) { + return nullptr; + } + + if (type == FileType::NSP) { + auto nsp = std::make_shared(file, program_id, program_index); + return nsp->GetStatus() == ResultStatus::Success ? nsp : nullptr; + } + + if (type == FileType::XCI) { + FileSys::XCI xci{file, program_id, program_index}; + if (xci.GetStatus() != ResultStatus::Success) { + return nullptr; + } + + auto secure_nsp = xci.GetSecurePartitionNSP(); + if (secure_nsp == nullptr || secure_nsp->GetStatus() != ResultStatus::Success) { + return nullptr; + } + + return secure_nsp; + } + + return nullptr; +} + +bool HasApplicationProgramContent(const std::shared_ptr& nsp) { + if (!nsp) { + return false; + } + + const auto& ncas = nsp->GetNCAs(); + return std::any_of(ncas.cbegin(), ncas.cend(), [](const auto& title_entry) { + const auto& nca_map = title_entry.second; + return nca_map.find( + {FileSys::TitleType::Application, FileSys::ContentRecordType::Program}) != + nca_map.end(); + }); +} + } // namespace FileType IdentifyFile(FileSys::VirtualFile file) { @@ -62,6 +109,27 @@ FileType IdentifyFile(FileSys::VirtualFile file) { } } +bool IsContainerType(FileType type) { + return type == FileType::NSP || type == FileType::XCI; +} + +bool IsBootableGameContainer(FileSys::VirtualFile file, FileType type, u64 program_id, + std::size_t program_index) { + if (!file) { + return false; + } + + if (type == FileType::Unknown) { + type = IdentifyFile(file); + } + + if (!IsContainerType(type)) { + return false; + } + + return HasApplicationProgramContent(OpenContainerAsNsp(file, type, program_id, program_index)); +} + FileType GuessFromFilename(const std::string& name) { if (name == "main") return FileType::DeconstructedRomDirectory; diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h index f4e932cec9..95ce638da0 100644 --- a/src/core/loader/loader.h +++ b/src/core/loader/loader.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -46,12 +49,29 @@ enum class FileType { }; /** - * Identifies the type of a bootable file based on the magic value in its header. + * Identifies the type of a supported file/container based on its structure. * @param file open file * @return FileType of file */ FileType IdentifyFile(FileSys::VirtualFile file); +/** + * Returns whether the file type represents a container format that can bundle multiple titles + * (currently NSP/XCI). + */ +bool IsContainerType(FileType type); + +/** + * Returns whether a container file is bootable as a game (has Application/Program content). + * + * @param file open file + * @param type optional file type; if Unknown it is auto-detected. + * @param program_id optional program id hint for multi-program containers. + * @param program_index optional program index hint for multi-program containers. + */ +bool IsBootableGameContainer(FileSys::VirtualFile file, FileType type = FileType::Unknown, + u64 program_id = 0, std::size_t program_index = 0); + /** * Guess the type of a bootable file from its name * @param name String name of bootable file diff --git a/src/core/loader/nsp.cpp b/src/core/loader/nsp.cpp index 3016d5f25f..4333acb70c 100644 --- a/src/core/loader/nsp.cpp +++ b/src/core/loader/nsp.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -55,19 +58,30 @@ AppLoader_NSP::~AppLoader_NSP() = default; FileType AppLoader_NSP::IdentifyType(const FileSys::VirtualFile& nsp_file) { const FileSys::NSP nsp(nsp_file); - if (nsp.GetStatus() == ResultStatus::Success) { - // Extracted Type case - if (nsp.IsExtractedType() && nsp.GetExeFS() != nullptr && - FileSys::IsDirectoryExeFS(nsp.GetExeFS())) { - return FileType::NSP; + if (nsp.GetStatus() != ResultStatus::Success) { + return FileType::Error; + } + + // Extracted Type case + if (nsp.IsExtractedType() && nsp.GetExeFS() != nullptr && + FileSys::IsDirectoryExeFS(nsp.GetExeFS())) { + return FileType::NSP; + } + + // Non-extracted NSPs can legitimately contain only update/DLC content. + // Identify the container format itself; bootability is validated by Load(). + if (!nsp.GetNCAs().empty()) { + return FileType::NSP; + } + + // Fallback when NCAs couldn't be parsed (e.g. missing keys) but the PFS still contains NCAs. + for (const auto& entry : nsp.GetFiles()) { + if (entry == nullptr) { + continue; } - // Non-Extracted Type case - const auto program_id = nsp.GetProgramTitleID(); - if (!nsp.IsExtractedType() && - nsp.GetNCA(program_id, FileSys::ContentRecordType::Program) != nullptr && - AppLoader_NCA::IdentifyType( - nsp.GetNCAFile(program_id, FileSys::ContentRecordType::Program)) == FileType::NCA) { + const auto& name = entry->GetName(); + if (name.size() >= 4 && name.substr(name.size() - 4) == ".nca") { return FileType::NSP; } } diff --git a/src/core/loader/xci.cpp b/src/core/loader/xci.cpp index e9abb199a1..983184a226 100644 --- a/src/core/loader/xci.cpp +++ b/src/core/loader/xci.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -44,10 +47,13 @@ AppLoader_XCI::~AppLoader_XCI() = default; FileType AppLoader_XCI::IdentifyType(const FileSys::VirtualFile& xci_file) { const FileSys::XCI xci(xci_file); - if (xci.GetStatus() == ResultStatus::Success && - xci.GetNCAByType(FileSys::NCAContentType::Program) != nullptr && - AppLoader_NCA::IdentifyType(xci.GetNCAFileByType(FileSys::NCAContentType::Program)) == - FileType::NCA) { + if (xci.GetStatus() != ResultStatus::Success) { + return FileType::Error; + } + + // Identify XCI as a valid container even when it does not include a bootable Program NCA. + // Bootability is handled by AppLoader_XCI::Load(). + if (xci.GetSecurePartitionNSP() != nullptr) { return FileType::XCI; } diff --git a/src/yuzu/game/game_list_worker.cpp b/src/yuzu/game/game_list_worker.cpp index c4504a0d5e..81012e4374 100644 --- a/src/yuzu/game/game_list_worker.cpp +++ b/src/yuzu/game/game_list_worker.cpp @@ -4,6 +4,7 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include #include #include @@ -16,14 +17,17 @@ #include "common/fs/fs.h" #include "common/fs/path_util.h" +#include "common/settings.h" #include "core/core.h" #include "core/file_sys/card_image.h" +#include "core/file_sys/common_funcs.h" #include "core/file_sys/content_archive.h" #include "core/file_sys/control_metadata.h" #include "core/file_sys/fs_filesystem.h" #include "core/file_sys/nca_metadata.h" #include "core/file_sys/patch_manager.h" #include "core/file_sys/registered_cache.h" +#include "core/file_sys/romfs.h" #include "core/file_sys/submission_package.h" #include "core/loader/loader.h" #include "yuzu/compatibility_list.h" @@ -375,6 +379,12 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa return true; } + if (target == ScanTarget::PopulateGameList && + (file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP) && + !Loader::IsBootableGameContainer(file, file_type)) { + return true; + } + u64 program_id = 0; const auto res2 = loader->ReadProgramId(program_id); @@ -383,18 +393,10 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa provider->AddEntry(FileSys::TitleType::Application, FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()), program_id, file); - } else if (res2 == Loader::ResultStatus::Success && + } else if (Settings::values.ext_content_from_game_dirs.GetValue() && (file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) { - const auto nsp = file_type == Loader::FileType::NSP - ? std::make_shared(file) - : FileSys::XCI{file}.GetSecurePartitionNSP(); - for (const auto& title : nsp->GetNCAs()) { - for (const auto& entry : title.second) { - provider->AddEntry(entry.first.first, entry.first.second, title.first, - entry.second->GetBaseFile()); - } - } + void(provider->AddEntriesFromContainer(file)); } } else { std::vector program_ids; diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp index 6ead3c4130..e02e02b413 100644 --- a/src/yuzu/main_window.cpp +++ b/src/yuzu/main_window.cpp @@ -2019,6 +2019,10 @@ void MainWindow::ConfigureFilesystemProvider(const std::string& filepath) { return; } + if (QtCommon::provider->AddEntriesFromContainer(file)) { + return; + } + auto loader = Loader::GetLoader(*QtCommon::system, file); if (!loader) { return; @@ -2033,19 +2037,8 @@ void MainWindow::ConfigureFilesystemProvider(const std::string& filepath) { const auto res2 = loader->ReadProgramId(program_id); if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) { QtCommon::provider->AddEntry(FileSys::TitleType::Application, - FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()), program_id, - file); - } else if (res2 == Loader::ResultStatus::Success && - (file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) { - const auto nsp = file_type == Loader::FileType::NSP - ? std::make_shared(file) - : FileSys::XCI{file}.GetSecurePartitionNSP(); - for (const auto& title : nsp->GetNCAs()) { - for (const auto& entry : title.second) { - QtCommon::provider->AddEntry(entry.first.first, entry.first.second, title.first, - entry.second->GetBaseFile()); - } - } + FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()), + program_id, file); } }