From dc1cd5aa3e7fee243e8a507f2e91233140318f03 Mon Sep 17 00:00:00 2001 From: cheezwiz7899 Date: Mon, 29 Jun 2026 10:23:45 +1000 Subject: [PATCH] refactor(content): consolidate update/DLC loading; fix version selection and sparse-base boot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the autoloader NCA folder scan, the Update Manager file-copy action, and the first-launch autoloader prompt. Consolidate all external update and DLC loading through ExternalContentProvider (Settings → General → External Content Dirs), which already handles multi-version tracking and correct version selection correctly. Fix five correctness bugs found during the audit: 1. Sparse-base titles (TOTK and similar) always booted the highest-priority NAND update regardless of the per-game disabled toggle, because the loader's fallback ExeFS path called ContentProvider::GetEntry() directly instead of consulting GetActiveUpdate(). The game would run 1.4.2 code even when 1.4.2 was unchecked and 1.2.1 external was enabled. (core/loader/nca.cpp) 2. GetPatches dedup comparison used the NACP display string ("1.4.2") for the NAND entry's .version but FormatTitleVersion hex format for canonical_version_str ("v1.4.2"), so the same version listed in both NAND and an external dir always produced two rows in the Add-ons UI. (core/file_sys/patch_manager.cpp) 3. AccumulateAOCTitleIDs did a flat std::transform with no disabled_addons filtering, making per-item DLC toggles in the Properties UI have no effect on what the aoc:u service reported to the game. CountAddOnContent and ListAddOnContent re-checked the global "DLC" key redundantly after the fact. (core/hle/service/aoc/addon_content_manager.cpp) 4. FindAutoloaderNCA was declared in patch_manager.h but never implemented anywhere — a latent linker error waiting to be called. (core/file_sys/patch_manager.h) 5. ContentProviderUnionSlot::Autoloader was declared in registered_cache.h but no provider was ever registered into that slot, making it dead and misleading enum noise. (core/file_sys/registered_cache.h) UI changes: - Remove File → Install Updates with Update Manager action - Remove Settings → Filesystem autoloader group (prompt checkbox + Run button) - Remove first-launch "would you like to run the Autoloader?" dialog - Add "Open" button to the External Content Dirs list in Settings → General - Pre-populate External Content Dirs with sdmc/citron/content/ on first run SD card mod loading (atmosphere/contents/), File → Install Files to NAND, and all existing External Content Dirs behaviour are unchanged. --- .../configuration/configure_filesystem.cpp | 151 ------ .../configuration/configure_filesystem.h | 6 - .../configuration/configure_filesystem.ui | 9 - .../configuration/configure_general.cpp | 7 + src/citron/configuration/configure_general.ui | 7 + src/citron/configuration/qt_config.cpp | 13 + src/citron/game_list.cpp | 24 - src/citron/game_list.h | 1 - src/citron/main.cpp | 348 +------------- src/citron/main.h | 4 - src/citron/main.ui | 9 - src/citron/uisettings.h | 1 - src/core/file_sys/patch_manager.cpp | 451 ++++-------------- src/core/file_sys/patch_manager.h | 1 - src/core/file_sys/registered_cache.h | 7 +- .../hle/service/aoc/addon_content_manager.cpp | 183 ++----- src/core/loader/nca.cpp | 41 +- 17 files changed, 213 insertions(+), 1050 deletions(-) diff --git a/src/citron/configuration/configure_filesystem.cpp b/src/citron/configuration/configure_filesystem.cpp index 1e31646f96..fd0b1bd486 100644 --- a/src/citron/configuration/configure_filesystem.cpp +++ b/src/citron/configuration/configure_filesystem.cpp @@ -10,24 +10,18 @@ #include #include #include -#include -#include "citron/main.h" #include "citron/uisettings.h" #include "common/fs/fs.h" #include "common/fs/path_util.h" #include "common/literals.h" #include "common/settings.h" -#include "frontend_common/content_manager.h" #include "ui_configure_filesystem.h" -static constexpr size_t ChunkCopyBufferSize = 0x400000; - ConfigureFilesystem::ConfigureFilesystem(QWidget* parent) : QWidget(parent), ui(std::make_unique()) { ui->setupUi(this); SetConfiguration(); - connect(ui->run_autoloader_button, &QPushButton::clicked, this, &ConfigureFilesystem::OnRunAutoloader); connect(ui->nand_directory_button, &QToolButton::pressed, this, [this] { SetDirectory(DirectoryTarget::NAND, ui->nand_directory_edit); }); connect(ui->sdmc_directory_button, &QToolButton::pressed, this, [this] { SetDirectory(DirectoryTarget::SD, ui->sdmc_directory_edit); }); connect(ui->gamecard_path_button, &QToolButton::pressed, this, [this] { SetDirectory(DirectoryTarget::Gamecard, ui->gamecard_path_edit); }); @@ -38,7 +32,6 @@ ConfigureFilesystem::ConfigureFilesystem(QWidget* parent) connect(ui->reset_game_list_cache, &QPushButton::pressed, this, &ConfigureFilesystem::ResetMetadata); connect(ui->gamecard_inserted, &QCheckBox::checkStateChanged, this, &ConfigureFilesystem::UpdateEnabledControls); connect(ui->gamecard_current_game, &QCheckBox::checkStateChanged, this, &ConfigureFilesystem::UpdateEnabledControls); - connect(this, &ConfigureFilesystem::UpdateInstallProgress, this, &ConfigureFilesystem::OnUpdateInstallProgress); #ifdef __linux__ connect(ui->enable_backups_checkbox, &QCheckBox::toggled, this, &ConfigureFilesystem::UpdateEnabledControls); @@ -74,7 +67,6 @@ void ConfigureFilesystem::SetConfiguration() { ui->dump_exefs->setChecked(Settings::values.dump_exefs.GetValue()); ui->dump_nso->setChecked(Settings::values.dump_nso.GetValue()); ui->cache_game_list->setChecked(UISettings::values.cache_game_list.GetValue()); - ui->prompt_for_autoloader->setChecked(UISettings::values.prompt_for_autoloader.GetValue()); ui->backup_saves_to_nand->setChecked(Settings::values.backup_saves_to_nand.GetValue()); // NCA Scanning Toggle @@ -106,7 +98,6 @@ void ConfigureFilesystem::ApplyConfiguration() { Settings::values.dump_exefs = ui->dump_exefs->isChecked(); Settings::values.dump_nso = ui->dump_nso->isChecked(); UISettings::values.cache_game_list = ui->cache_game_list->isChecked(); - UISettings::values.prompt_for_autoloader = ui->prompt_for_autoloader->isChecked(); Settings::values.backup_saves_to_nand.SetValue(ui->backup_saves_to_nand->isChecked()); // NCA Scanning Toggle @@ -323,148 +314,6 @@ void ConfigureFilesystem::MigrateBackups(const QString& old_path, const QString& } #endif -void ConfigureFilesystem::OnUpdateInstallProgress() { - if (install_progress) { - install_progress->setValue(install_progress->value() + 1); - } -} - -void ConfigureFilesystem::OnRunAutoloader(bool skip_confirmation) { - if (!skip_confirmation) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Begin Autoloader?")); - msgBox.setText(tr("The Autoloader will scan your Game Directories for all .nsp files " - "and attempt to install any found updates or DLC. This may take a while.")); - msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); - msgBox.setDefaultButton(QMessageBox::Ok); - if (msgBox.exec() != QMessageBox::Ok) { - return; - } - } - - GMainWindow* main_window = qobject_cast(this->parent()); - if (!main_window) { - main_window = qobject_cast(this->window()->parent()); - } - - if (!main_window) { - QMessageBox::critical(this, tr("Error"), tr("Could not find the main window.")); - return; - } - Core::System* system = main_window->GetSystem(); - const auto& vfs = main_window->GetVFS(); - if (!system) { - QMessageBox::critical(this, tr("Error"), tr("System is not initialized.")); - return; - } - - QStringList files_to_install; - for (const auto& game_dir : UISettings::values.game_dirs) { - Common::FS::IterateDirEntriesRecursively(game_dir.path, [&](const auto& entry) { - if (!entry.is_directory() && entry.path().extension() == ".nsp") { - files_to_install.append(QString::fromStdString(entry.path().string())); - } - return true; - }); - } - - if (files_to_install.isEmpty()) { - QMessageBox::information(this, tr("Autoloader"), tr("No .nsp files found to install.")); - return; - } - - qint64 total_chunks = 0; - for (const QString& file : files_to_install) { - total_chunks += (QFileInfo(file).size() + ChunkCopyBufferSize - 1) / ChunkCopyBufferSize; - } - if (total_chunks == 0) { - QMessageBox::information(this, tr("Autoloader"), tr("Selected files are empty.")); - return; - } - - QStringList new_files{}, overwritten_files{}, failed_files{}; - bool detected_base_install{}; - bool was_cancelled = false; - - install_progress = new QProgressDialog(QString{}, tr("Cancel"), 0, static_cast(total_chunks), this); - install_progress->setWindowFlags(install_progress->windowFlags() & ~Qt::WindowContextHelpButtonHint); - install_progress->setAttribute(Qt::WA_DeleteOnClose, true); - install_progress->setFixedWidth(400); - connect(install_progress, &QObject::destroyed, this, [this]() { install_progress = nullptr; }); - install_progress->show(); - - int remaining = files_to_install.size(); - for (const QString& file : files_to_install) { - if (!install_progress || install_progress->wasCanceled()) { - was_cancelled = true; - break; - } - - install_progress->setWindowTitle(tr("Autoloader - %n file(s) remaining", "", remaining)); - install_progress->setLabelText(tr("Installing: %1").arg(QFileInfo(file).fileName())); - - auto progress_callback = [this](size_t, size_t) { - emit UpdateInstallProgress(); - if (!install_progress) return true; - return install_progress->wasCanceled(); - }; - - QFuture future = - QtConcurrent::run([&] { return ContentManager::InstallNSP(*system, *vfs, file.toStdString(), progress_callback); }); - - while (!future.isFinished()) { - QCoreApplication::processEvents(); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - - ContentManager::InstallResult result = future.result(); - - switch (result) { - case ContentManager::InstallResult::Success: - new_files.append(QFileInfo(file).fileName()); - break; - case ContentManager::InstallResult::Overwrite: - overwritten_files.append(QFileInfo(file).fileName()); - break; - case ContentManager::InstallResult::Failure: - failed_files.append(QFileInfo(file).fileName()); - break; - case ContentManager::InstallResult::BaseInstallAttempted: - failed_files.append(QFileInfo(file).fileName()); - detected_base_install = true; - break; - } - --remaining; - } - - if (install_progress) { - install_progress->close(); - } - - if (detected_base_install) { - QMessageBox::warning(this, tr("Install Results"), tr("Warning: Base games were detected and skipped. The autoloader is intended for updates and DLC.")); - } - - if (new_files.isEmpty() && overwritten_files.isEmpty() && failed_files.isEmpty()) { - if (!was_cancelled) { - QMessageBox::information(this, tr("Autoloader"), tr("No new files were installed.")); - } - } else { - QString install_results = tr("Installation Complete!"); - install_results.append(QLatin1String("\n\n")); - if (!new_files.isEmpty()) - install_results.append(tr("%n file(s) were newly installed.", nullptr, new_files.size())); - if (!overwritten_files.isEmpty()) - install_results.append(tr("\n%n file(s) were overwritten.", nullptr, overwritten_files.size())); - if (!failed_files.isEmpty()) - install_results.append(tr("\n%n file(s) failed to install.", nullptr, failed_files.size())); - QMessageBox::information(this, tr("Install Results"), install_results); - } - - Common::FS::RemoveDirRecursively(Common::FS::GetCitronPath(Common::FS::CitronPath::CacheDir) / "game_list"); - emit RequestGameListRefresh(); -} - void ConfigureFilesystem::MigrateSavesToGlobal(const QString& new_global_path) { const QString nand_root = QString::fromStdString(Common::FS::GetCitronPathString(Common::FS::CitronPath::NANDDir)); const QString global_root = new_global_path; diff --git a/src/citron/configuration/configure_filesystem.h b/src/citron/configuration/configure_filesystem.h index ffbd45bf0e..d2a5b3a871 100644 --- a/src/citron/configuration/configure_filesystem.h +++ b/src/citron/configuration/configure_filesystem.h @@ -22,15 +22,10 @@ public: ~ConfigureFilesystem() override; void ApplyConfiguration(); - void OnRunAutoloader(bool skip_confirmation = false); signals: - void UpdateInstallProgress(); void RequestGameListRefresh(); -private slots: - void OnUpdateInstallProgress(); - private: void changeEvent(QEvent* event) override; void RetranslateUI(); @@ -45,7 +40,6 @@ private: bool CopyDirRecursive(const QString& src, const QString& dest, QProgressDialog& progress, qint64& copied, qint64 total); std::unique_ptr ui; - QProgressDialog* install_progress = nullptr; bool m_old_custom_backup_enabled{}; QString m_old_backup_path; diff --git a/src/citron/configuration/configure_filesystem.ui b/src/citron/configuration/configure_filesystem.ui index 51b6fc1fef..01986fb2ed 100644 --- a/src/citron/configuration/configure_filesystem.ui +++ b/src/citron/configuration/configure_filesystem.ui @@ -77,15 +77,6 @@ - - - Autoloader - - Prompt to run Autoloader when a new game directory is added - Run Autoloader Now - - - Updater diff --git a/src/citron/configuration/configure_general.cpp b/src/citron/configuration/configure_general.cpp index 2dd8c57b1a..b5868f2171 100644 --- a/src/citron/configuration/configure_general.cpp +++ b/src/citron/configuration/configure_general.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -49,6 +50,12 @@ ConfigureGeneral::ConfigureGeneral(Core::System& system_, &ConfigureGeneral::AddExternalContentDir); connect(ui->button_remove_external_content, &QPushButton::clicked, this, &ConfigureGeneral::RemoveExternalContentDir); + connect(ui->button_open_external_content, &QPushButton::clicked, this, [this] { + const auto* item = ui->external_content_list->currentItem(); + if (!item) + return; + QDesktopServices::openUrl(QUrl::fromLocalFile(item->text())); + }); } ConfigureGeneral::~ConfigureGeneral() = default; diff --git a/src/citron/configuration/configure_general.ui b/src/citron/configuration/configure_general.ui index e6b676dd69..348a1946d9 100644 --- a/src/citron/configuration/configure_general.ui +++ b/src/citron/configuration/configure_general.ui @@ -156,6 +156,13 @@ + + + + Open + + + diff --git a/src/citron/configuration/qt_config.cpp b/src/citron/configuration/qt_config.cpp index 1cf26e3861..6288d46da9 100644 --- a/src/citron/configuration/qt_config.cpp +++ b/src/citron/configuration/qt_config.cpp @@ -2,6 +2,7 @@ // SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include "common/fs/path_util.h" #include "common/logging.h" #include "input_common/main.h" #include "qt_config.h" @@ -254,6 +255,18 @@ void QtConfig::ReadPathValues() { ReadCategory(Settings::Category::Paths); + // On first run (or if the user has never configured external content dirs), + // pre-populate with the default sdmc/citron/content directory so NSPs dropped + // there are picked up automatically by ExternalContentProvider. + // Placed after ReadCategory(Paths) so Settings::values.sdmc_dir is resolved + // before we call GetCitronPath(SDMCDir). + if (Settings::values.external_content_dirs.empty()) { + const auto default_content_dir = + Common::FS::GetCitronPath(Common::FS::CitronPath::SDMCDir) / "citron" / "content"; + Settings::values.external_content_dirs.push_back( + Common::FS::PathToUTF8String(default_content_dir)); + } + EndGroup(); } diff --git a/src/citron/game_list.cpp b/src/citron/game_list.cpp index 671c3c8653..967374ec8a 100644 --- a/src/citron/game_list.cpp +++ b/src/citron/game_list.cpp @@ -2861,30 +2861,6 @@ void GameList::ValidateEntry(const QModelIndex& item) { } case GameListItemType::AddDir: emit AddDirectory(); - - if (UISettings::values.prompt_for_autoloader) { - QMessageBox msg_box(this); - msg_box.setWindowTitle(tr("Autoloader")); - msg_box.setText( - tr("Would you like to use the Autoloader to install all Updates/DLC within your " - "game directories?\n\n" - "If not now, you can always go to Emulation -> Configure -> Filesystem in order " - "to use this feature. Also, if you have multiple update files for a single " - "game, you can use the Update Manager " - "in File -> Install Updates with Update Manager.")); - msg_box.setStandardButtons(QMessageBox::Yes | QMessageBox::No); - QCheckBox* check_box = new QCheckBox(tr("Do not ask me again")); - msg_box.setCheckBox(check_box); - - if (msg_box.exec() == QMessageBox::Yes) { - emit RunAutoloaderRequested(); - } - - if (check_box->isChecked()) { - UISettings::values.prompt_for_autoloader = false; - emit SaveConfig(); - } - } break; default: break; diff --git a/src/citron/game_list.h b/src/citron/game_list.h index 5de41afd7c..22d74f0370 100644 --- a/src/citron/game_list.h +++ b/src/citron/game_list.h @@ -176,7 +176,6 @@ signals: void ShowList(bool show); void PopulatingCompleted(); void SaveConfig(); - void RunAutoloaderRequested(); public slots: void OnConfigurationChanged(); diff --git a/src/citron/main.cpp b/src/citron/main.cpp index 6d739424c0..14c58e30a4 100644 --- a/src/citron/main.cpp +++ b/src/citron/main.cpp @@ -452,11 +452,7 @@ GMainWindow::GMainWindow(std::unique_ptr config_, bool has_broken_vulk // 1. First, create the factories LOG_INFO(Frontend, "Initializing factories..."); - system->GetFileSystemController().InitializeContentSystem(*vfs); - - autoloader_provider = std::make_unique(); - system->RegisterContentProvider(FileSys::ContentProviderUnionSlot::Autoloader, - autoloader_provider.get()); + system->GetFileSystemController().CreateFactories(*vfs); // Remove cached contents generated during the previous session RemoveCachedContents(); @@ -1796,8 +1792,6 @@ void GMainWindow::ConnectWidgetEvents() { connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); - connect(game_list, &GameList::RunAutoloaderRequested, this, - &GMainWindow::OnRunAutoloaderFromGameList); connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList); connect(game_list, &GameList::PopulatingCompleted, [this] { multiplayer_state->UpdateGameList(game_list->GetModel()); }); @@ -1882,8 +1876,6 @@ void GMainWindow::ConnectMenuEvents() { connect_menu(ui->action_Load_File, &GMainWindow::OnMenuLoadFile); connect_menu(ui->action_Load_Folder, &GMainWindow::OnMenuLoadFolder); connect_menu(ui->action_Install_File_NAND, &GMainWindow::OnMenuInstallToNAND); - connect(ui->action_Install_With_Update_Manager, &QAction::triggered, this, - &GMainWindow::OnMenuInstallWithUpdateManager); connect_menu(ui->action_Trim_XCI_File, &GMainWindow::OnMenuTrimXCI); connect_menu(ui->action_Exit, &QMainWindow::close); connect_menu(ui->action_Load_Amiibo, &GMainWindow::OnLoadAmiibo); @@ -2299,8 +2291,6 @@ void GMainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletP game_list->CancelPopulation(); game_list->ClearLaunchOverlays(); - RegisterAutoloaderContents(); - if (params.program_id == 0 || params.program_id > static_cast(Service::AM::AppletProgramId::MaxProgramId)) { StoreRecentFile(filename); // Put the filename on top of the list @@ -2556,7 +2546,7 @@ void GMainWindow::OnEmulationStopped() { game_list->CancelPopulation(); // This is necessary to reset the in-memory state for the next launch. - system->GetFileSystemController().InitializeContentSystem(*vfs, true); + system->GetFileSystemController().CreateFactories(*vfs, true); // Refresh the game list now that the filesystem is valid again. game_list->ClearLaunchOverlays(); @@ -5130,7 +5120,7 @@ void GMainWindow::OnInstallFirmwareFromZip() { } // Re-scan VFS for the newly placed firmware files. - system->GetFileSystemController().InitializeContentSystem(*vfs); + system->GetFileSystemController().CreateFactories(*vfs); auto VerifyFirmwareCallback = [&](size_t total_size, size_t processed_size) { progress.setValue(85 + static_cast((processed_size * 15) / total_size)); @@ -5294,7 +5284,7 @@ void GMainWindow::OnInstallFirmware() { } // Re-scan VFS for the newly placed firmware files. - system->GetFileSystemController().InitializeContentSystem(*vfs); + system->GetFileSystemController().CreateFactories(*vfs); auto VerifyFirmwareCallback = [&](size_t total_size, size_t processed_size) { progress.setValue(90 + static_cast((processed_size * 10) / total_size)); @@ -5391,7 +5381,7 @@ void GMainWindow::OnInstallDecryptionKeys() { // and re-populate the game list in the UI if the user has already added // game folders. Core::Crypto::KeyManager::Instance().ReloadKeys(); - system->GetFileSystemController().InitializeContentSystem(*vfs); + system->GetFileSystemController().CreateFactories(*vfs); game_list->PopulateAsync(UISettings::values.game_dirs); if (ContentManager::AreKeysPresent()) { @@ -6066,7 +6056,7 @@ void GMainWindow::OnMouseActivity() { } void GMainWindow::OnCheckFirmwareDecryption() { - system->GetFileSystemController().InitializeContentSystem(*vfs); + system->GetFileSystemController().CreateFactories(*vfs); if (!ContentManager::AreKeysPresent()) { QMessageBox::warning(this, tr("Derivation Components Missing"), tr("Encryption keys are missing. " @@ -7213,332 +7203,6 @@ void GMainWindow::CheckForUpdatesAutomatically() { #endif } -void GMainWindow::RegisterAutoloaderContents() { - autoloader_provider->ClearAllEntries(); - - const auto sdmc_path = Common::FS::GetCitronPath(Common::FS::CitronPath::SDMCDir); - const auto autoloader_root = sdmc_path / "autoloader"; - if (!Common::FS::IsDir(autoloader_root)) { - return; - } - - LOG_INFO(Frontend, "Scanning for Autoloader contents..."); - - for (const auto& title_dir_entry : std::filesystem::directory_iterator(autoloader_root)) { - if (!title_dir_entry.is_directory()) - continue; - - try { - [[maybe_unused]] auto val = - std::stoull(title_dir_entry.path().filename().string(), nullptr, 16); - } catch (const std::invalid_argument&) { - continue; - } - - const auto process_content_type = [&](const std::filesystem::path& content_path) { - if (!Common::FS::IsDir(content_path)) - return; - - for (const auto& mod_dir_entry : std::filesystem::directory_iterator(content_path)) { - if (!mod_dir_entry.is_directory()) - continue; - - const std::string mod_name = mod_dir_entry.path().filename().string(); - // We do NOT skip disabled content here. - // If we skip it here, it doesn't show up in the UI (Properties -> Add-ons), - // making it impossible for the user to re-enable it. - // The PatchManager (core/file_sys/patch_manager.cpp) handles the actual enforcement - // of disabled status during game load. - - std::optional cnmt; - for (const auto& file_entry : - std::filesystem::directory_iterator(mod_dir_entry.path())) { - if (file_entry.path().string().ends_with(".cnmt.nca")) { - auto vfs_file = - vfs->OpenFile(file_entry.path().string(), FileSys::OpenMode::Read); - if (vfs_file) { - FileSys::NCA meta_nca(vfs_file); - if (meta_nca.GetStatus() == Loader::ResultStatus::Success && - !meta_nca.GetSubdirectories().empty()) { - auto section0 = meta_nca.GetSubdirectories()[0]; - if (!section0->GetFiles().empty()) { - cnmt.emplace(section0->GetFiles()[0]); - break; - } - } - } - } - } - - if (!cnmt) - continue; - - for (const auto& record : cnmt->GetContentRecords()) { - std::string nca_filename = Common::HexToString(record.nca_id) + ".nca"; - std::filesystem::path nca_path = mod_dir_entry.path() / nca_filename; - auto nca_vfs_file = vfs->OpenFile(nca_path.string(), FileSys::OpenMode::Read); - if (nca_vfs_file) { - autoloader_provider->AddEntry(cnmt->GetType(), record.type, - cnmt->GetTitleID(), nca_vfs_file); - } - } - } - }; - - process_content_type(title_dir_entry.path() / "Updates"); - process_content_type(title_dir_entry.path() / "DLC"); - } -} - -void GMainWindow::OnMenuInstallWithUpdateManager() { - LOG_INFO(Loader, "UPDATE MANAGER: Starting update installation process."); - - const QString file_filter = tr("Nintendo Submission Package (*.nsp)"); - QStringList filenames = QFileDialog::getOpenFileNames( - this, tr("Select Update Files for Update Manager"), - QString::fromStdString(UISettings::values.roms_path), file_filter); - - if (filenames.isEmpty()) { - return; - } - - UISettings::values.roms_path = QFileInfo(filenames[0]).path().toStdString(); - - bool dlc_detected = false; - for (const QString& file : filenames) { - QString sanitized_path = file; - if (sanitized_path.contains(QLatin1String(".nsp/"))) { - sanitized_path = - sanitized_path.left(sanitized_path.indexOf(QLatin1String(".nsp/")) + 4); - } - auto vfs_file = vfs->OpenFile(sanitized_path.toStdString(), FileSys::OpenMode::Read); - if (vfs_file) { - FileSys::NSP nsp(vfs_file); - auto nsp_ncas = nsp.GetNCAs(); - if (nsp.GetStatus() == Loader::ResultStatus::Success && !nsp_ncas.empty()) { - const auto& [title_id, nca_map] = *nsp_ncas.begin(); - const auto meta_iter = - std::find_if(nca_map.begin(), nca_map.end(), [](const auto& pair) { - return pair.first.second == FileSys::ContentRecordType::Meta; - }); - - if (meta_iter != nca_map.end()) { - const auto& meta_nca = meta_iter->second; - if (meta_nca && !meta_nca->GetSubdirectories().empty() && - !meta_nca->GetSubdirectories()[0]->GetFiles().empty()) { - const auto cnmt_file = meta_nca->GetSubdirectories()[0]->GetFiles()[0]; - const FileSys::CNMT cnmt(cnmt_file); - if (cnmt.GetType() != FileSys::TitleType::Update) { - dlc_detected = true; - break; // Found one DLC, no need to check further. - } - } - } - } - } - } - - if (dlc_detected) { - QMessageBox::warning(this, tr("DLC Detected"), - tr("The Update Manager is not compatible with DLC installations. " - "Please select only update files.")); - return; // Abort the operation. - } - - qint64 total_size_bytes = 0; - for (const QString& file : filenames) { - QString sanitized_path = file; - if (sanitized_path.contains(QLatin1String(".nsp/"))) { - sanitized_path = - sanitized_path.left(sanitized_path.indexOf(QLatin1String(".nsp/")) + 4); - } - auto vfs_file = vfs->OpenFile(sanitized_path.toStdString(), FileSys::OpenMode::Read); - if (vfs_file) { - FileSys::NSP nsp(vfs_file); - if (nsp.GetStatus() == Loader::ResultStatus::Success) { - for (const auto& title_pair : nsp.GetNCAs()) { - for (const auto& nca_pair : title_pair.second) { - total_size_bytes += nca_pair.second->GetBaseFile()->GetSize(); - } - } - } - } - } - - if (total_size_bytes == 0) { - QMessageBox::warning(this, tr("No files to install"), - tr("Could not find any valid files to install in the selected NSPs.")); - return; - } - - QProgressDialog progress(tr("Installing Updates..."), tr("Cancel"), 0, 100, this); - progress.setWindowModality(Qt::WindowModal); - progress.setMinimumDuration(0); - progress.setValue(0); - progress.show(); - - qint64 total_copied_bytes = 0; - int success_count = 0; - QStringList failed_files; - - for (const QString& file : filenames) { - progress.setLabelText(tr("Installing %1...").arg(QFileInfo(file).fileName())); - QCoreApplication::processEvents(); - - if (progress.wasCanceled()) { - break; - } - - QString sanitized_path = file; - if (sanitized_path.contains(QLatin1String(".nsp/"))) { - sanitized_path = - sanitized_path.left(sanitized_path.indexOf(QLatin1String(".nsp/")) + 4); - } - const std::string file_path = sanitized_path.toStdString(); - LOG_INFO(Loader, "UPDATE MANAGER: Processing sanitized file path: {}", file_path); - - auto vfs_file = vfs->OpenFile(file_path, FileSys::OpenMode::Read); - if (!vfs_file) { - LOG_ERROR(Loader, "UPDATE MANAGER: FAILED at VFS Open. Could not open file: {}", - file_path); - failed_files.append(QFileInfo(file).fileName() + tr(" (File Open Error)")); - continue; - } - - FileSys::NSP nsp(vfs_file); - if (nsp.GetStatus() != Loader::ResultStatus::Success) { - LOG_ERROR(Loader, "UPDATE MANAGER: FAILED at NSP Parse for file: {}", file_path); - failed_files.append(QFileInfo(file).fileName() + tr(" (NSP Parse Error)")); - continue; - } - - const auto title_map = nsp.GetNCAs(); - if (title_map.empty()) { - LOG_ERROR(Loader, "UPDATE MANAGER: FAILED, NSP contains no titles: {}", file_path); - failed_files.append(QFileInfo(file).fileName() + tr(" (Empty NSP)")); - continue; - } - - const auto& [title_id, nca_map] = *title_map.begin(); - const auto& [type_pair, meta_nca] = - *std::find_if(nca_map.begin(), nca_map.end(), [](const auto& pair) { - return pair.first.second == FileSys::ContentRecordType::Meta; - }); - - if (!meta_nca || meta_nca->GetSubdirectories().empty() || - meta_nca->GetSubdirectories()[0]->GetFiles().empty()) { - LOG_ERROR(Loader, "UPDATE MANAGER: FAILED at Metadata search for title {}: malformed.", - title_id); - failed_files.append(QFileInfo(file).fileName() + tr(" (Malformed Metadata)")); - continue; - } - - const auto cnmt_file = meta_nca->GetSubdirectories()[0]->GetFiles()[0]; - const FileSys::CNMT cnmt(cnmt_file); - - std::string type_folder = "Updates"; - u64 program_id = FileSys::GetBaseTitleID(title_id); - QString nsp_name = QFileInfo(sanitized_path).completeBaseName(); - std::string sdmc_path = Common::FS::GetCitronPathString(Common::FS::CitronPath::SDMCDir); - std::string dest_path_str = fmt::format("{}/autoloader/{:016X}/{}/{}", sdmc_path, - program_id, type_folder, nsp_name.toStdString()); - - auto dest_dir = - VfsFilesystemCreateDirectoryWrapper(vfs, dest_path_str, FileSys::OpenMode::ReadWrite); - if (!dest_dir) { - LOG_ERROR(Loader, "UPDATE MANAGER: FAILED to create destination directory: {}", - dest_path_str); - failed_files.append(QFileInfo(file).fileName() + tr(" (Directory Creation Error)")); - continue; - } - - bool copy_failed = false; - for (const auto& [key, nca] : nca_map) { - auto source_file = nca->GetBaseFile(); - auto dest_file = dest_dir->CreateFileRelative(source_file->GetName()); - - if (!dest_file) { - LOG_ERROR(Loader, "UPDATE MANAGER: FAILED to create destination file for {}.", - source_file->GetName()); - copy_failed = true; - break; - } - - if (!dest_file->Resize(source_file->GetSize())) { - LOG_ERROR(Loader, "UPDATE MANAGER: FAILED to resize destination file for {}.", - source_file->GetName()); - copy_failed = true; - break; - } - - std::vector buffer(CopyBufferSize); - for (std::size_t i = 0; i < source_file->GetSize(); i += buffer.size()) { - if (progress.wasCanceled()) { - dest_file->Resize(0); - copy_failed = true; - break; - } - - const auto bytes_to_read = std::min(buffer.size(), source_file->GetSize() - i); - const auto bytes_read = source_file->Read(buffer.data(), bytes_to_read, i); - - if (bytes_read == 0 && i < source_file->GetSize()) { - LOG_ERROR(Loader, "UPDATE MANAGER: FAILED to read from source file {}.", - source_file->GetName()); - copy_failed = true; - break; - } - - dest_file->Write(buffer.data(), bytes_read, i); - - total_copied_bytes += bytes_read; - progress.setValue((total_copied_bytes * 100) / total_size_bytes); - QCoreApplication::processEvents(); - } - - if (copy_failed) { - break; - } - } - - if (progress.wasCanceled()) { - failed_files.append(QFileInfo(file).fileName() + tr(" (Cancelled)")); - vfs->DeleteDirectory(dest_path_str); - break; - } - - if (copy_failed) { - failed_files.append(QFileInfo(file).fileName()); - vfs->DeleteDirectory(dest_path_str); - } else { - success_count++; - } - } - - progress.close(); - - QString message = tr("Update Manager install finished."); - if (success_count > 0) { - message += tr("\n%n file(s) successfully installed.", "", success_count); - } - if (!failed_files.isEmpty()) { - message += tr("\n%n file(s) failed to install:", "", failed_files.size()); - message += QStringLiteral("\n- ") + failed_files.join(QStringLiteral("\n- ")); - } - QMessageBox::information(this, tr("Install Complete"), message); - - RegisterAutoloaderContents(); - game_list->PopulateAsync(UISettings::values.game_dirs); -} - void GMainWindow::OnToggleGridView() { game_list->ToggleViewMode(); } - -void GMainWindow::OnRunAutoloaderFromGameList() { - // This creates a temporary instance of the filesystem logic, - // calls the autoloader function, and then the instance is automatically cleaned up. - // We pass 'this' (the GMainWindow) as the parent. - ConfigureFilesystem fs_logic(this); - fs_logic.OnRunAutoloader(true); -} diff --git a/src/citron/main.h b/src/citron/main.h index 90ada6b1c5..135a86afc2 100644 --- a/src/citron/main.h +++ b/src/citron/main.h @@ -273,7 +273,6 @@ private: void LinkActionShortcut(QAction* action, const QString& action_name, const bool tas_allowed = false); void RegisterMetaTypes(); - void RegisterAutoloaderContents(); void InitializeWidgets(); void InitializeDebugWidgets(); void InitializeRecentFileMenuActions(); @@ -316,7 +315,6 @@ private: Service::AM::FrontendAppletParameters SystemAppletParameters(u64 program_id, Service::AM::AppletId applet_id); void SetupHomeMenuCallback(); - std::unique_ptr autoloader_provider; u64 current_title_id{0}; private slots: void OnStartGame(); @@ -350,8 +348,6 @@ private slots: void OnMenuLoadFolder(); void OnMenuInstallToNAND(); void OnMenuTrimXCI(); - void OnMenuInstallWithUpdateManager(); - void OnRunAutoloaderFromGameList(); void OnMenuRecentFile(); void OnConfigure(); void OnConfigureTas(); diff --git a/src/citron/main.ui b/src/citron/main.ui index 0525ee542a..1eb469fc14 100644 --- a/src/citron/main.ui +++ b/src/citron/main.ui @@ -59,7 +59,6 @@ - @@ -215,14 +214,6 @@ &Install Files to NAND... - - - true - - - Install Updates with &Update Manager - - true diff --git a/src/citron/uisettings.h b/src/citron/uisettings.h index 01c8561e14..2dd5564ba9 100644 --- a/src/citron/uisettings.h +++ b/src/citron/uisettings.h @@ -250,7 +250,6 @@ namespace UISettings { std::atomic_bool is_game_list_reload_pending{false}; Setting cache_game_list{linkage, true, "cache_game_list", Category::UiGameList}; Setting scan_nca{linkage, false, "scan_nca", Category::UiGameList}; - Setting prompt_for_autoloader{linkage, true, "prompt_for_autoloader", Category::UiGameList}; Setting favorites_expanded{linkage, true, "favorites_expanded", Category::UiGameList}; QVector favorited_ids; QStringList hidden_paths; diff --git a/src/core/file_sys/patch_manager.cpp b/src/core/file_sys/patch_manager.cpp index df950da0fb..01df0330f7 100644 --- a/src/core/file_sys/patch_manager.cpp +++ b/src/core/file_sys/patch_manager.cpp @@ -47,7 +47,7 @@ namespace FileSys { namespace { // patch_manager.cpp only - const ContentProviderUnion* GetUnionProvider(const ContentProvider& p) { +inline const ContentProviderUnion* GetUnionProvider(const ContentProvider& p) { return p.IsUnionProvider() ? static_cast(&p) : nullptr; @@ -195,93 +195,54 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const { return exefs; const auto& disabled = Settings::values.disabled_addons[title_id]; - bool autoloader_update_applied = false; - // --- AUTOLOADER UPDATE (PRIORITY) --- - VirtualDir sdmc_root = nullptr; - if (fs_controller.OpenSDMC(&sdmc_root).IsSuccess() && sdmc_root) { - const auto autoloader_updates_path = fmt::format("autoloader/{:016X}/Updates", title_id); - const auto updates_dir = sdmc_root->GetSubdirectory(autoloader_updates_path); - if (updates_dir) { - const auto base_program_nca = - content_provider.GetEntry(title_id, ContentRecordType::Program); - if (base_program_nca) { - for (const auto& mod : updates_dir->GetSubdirectories()) { - if (mod && std::find(disabled.cbegin(), disabled.cend(), mod->GetName()) == - disabled.cend()) { - for (const auto& file : mod->GetFiles()) { - if (file->GetExtension() == "nca") { - NCA nca(file, base_program_nca.get()); - if (nca.GetStatus() == Loader::ResultStatus::Success && - nca.GetType() == NCAContentType::Program) { - LOG_INFO( - Loader, - " ExeFS: Autoloader Update ({}) applied successfully", - mod->GetName()); - exefs = nca.GetExeFS(); - autoloader_update_applied = true; - break; - } - } - } - } - if (autoloader_update_applied) - break; - } + // --- NAND/External UPDATE --- + const auto active_update = GetActiveUpdate(); + u32 best_version = active_update.version; + VirtualFile best_update_raw = nullptr; + bool found_best = active_update.found; + + if (found_best) { + const auto update_tid = GetUpdateTitleID(title_id); + if (active_update.is_external) { + const auto* content_provider_union = GetUnionProvider(content_provider); + if (content_provider_union) { + best_update_raw = content_provider_union->GetExternalEntryForVersion( + update_tid, ContentRecordType::Program, best_version); } + } else { + best_update_raw = content_provider.GetEntryRaw(update_tid, ContentRecordType::Program); } } - // --- NAND UPDATE (FALLBACK) --- - // --- NAND/External UPDATE (FALLBACK) --- - if (!autoloader_update_applied) { - const auto active_update = GetActiveUpdate(); - u32 best_version = active_update.version; - VirtualFile best_update_raw = nullptr; - bool found_best = active_update.found; + LOG_INFO(Loader, "PatchExeFS: found_best={}, best_version={}, best_update_raw is {}", + found_best, best_version, best_update_raw != nullptr ? "VALID" : "NULL"); - if (found_best) { - const auto update_tid = GetUpdateTitleID(title_id); - if (active_update.is_external) { - const auto* content_provider_union = GetUnionProvider(content_provider); - if (content_provider_union) { - best_update_raw = content_provider_union->GetExternalEntryForVersion( - update_tid, ContentRecordType::Program, best_version); - } - } else { - best_update_raw = content_provider.GetEntryRaw(update_tid, ContentRecordType::Program); + if (found_best && best_update_raw != nullptr) { + const auto base_program_nca = + content_provider.GetEntry(title_id, ContentRecordType::Program); + LOG_INFO(Loader, "PatchExeFS: base_program_nca is {}", + base_program_nca != nullptr ? "VALID" : "NULL"); + + if (base_program_nca) { + const auto new_nca = std::make_shared(best_update_raw, base_program_nca.get()); + LOG_INFO(Loader, "PatchExeFS: new_nca status={}, has ExeFS={}", + static_cast(new_nca->GetStatus()), new_nca->GetExeFS() != nullptr); + if (new_nca->GetStatus() == Loader::ResultStatus::Success && + new_nca->GetExeFS() != nullptr) { + LOG_INFO(Loader, " ExeFS: Update ({}) applied successfully", + FormatTitleVersion(best_version)); + exefs = new_nca->GetExeFS(); } - } - - LOG_INFO(Loader, "PatchExeFS: found_best={}, best_version={}, best_update_raw is {}", found_best, best_version, best_update_raw != nullptr ? "VALID" : "NULL"); - - // Apply the best update found - if (found_best && best_update_raw != nullptr) { - // We need base_program_nca for patching - const auto base_program_nca = - content_provider.GetEntry(title_id, ContentRecordType::Program); - LOG_INFO(Loader, "PatchExeFS: base_program_nca is {}", base_program_nca != nullptr ? "VALID" : "NULL"); - - if (base_program_nca) { - const auto new_nca = std::make_shared(best_update_raw, base_program_nca.get()); - LOG_INFO(Loader, "PatchExeFS: new_nca status={}, has ExeFS={}", static_cast(new_nca->GetStatus()), new_nca->GetExeFS() != nullptr); - if (new_nca->GetStatus() == Loader::ResultStatus::Success && - new_nca->GetExeFS() != nullptr) { - LOG_INFO(Loader, " ExeFS: Update ({}) applied successfully", - FormatTitleVersion(best_version)); - exefs = new_nca->GetExeFS(); - } - } else { - // Fallback if no base program (unlikely for patching ExeFS) - // Or if it's a type that doesn't strictly need base? - const auto new_nca = std::make_shared(best_update_raw, nullptr); - LOG_INFO(Loader, "PatchExeFS (No Base): new_nca status={}, has ExeFS={}", static_cast(new_nca->GetStatus()), new_nca->GetExeFS() != nullptr); - if (new_nca->GetStatus() == Loader::ResultStatus::Success && - new_nca->GetExeFS() != nullptr) { - LOG_INFO(Loader, " ExeFS: Update ({}) applied successfully (No Base)", - FormatTitleVersion(best_version)); - exefs = new_nca->GetExeFS(); - } + } else { + const auto new_nca = std::make_shared(best_update_raw, nullptr); + LOG_INFO(Loader, "PatchExeFS (No Base): new_nca status={}, has ExeFS={}", + static_cast(new_nca->GetStatus()), new_nca->GetExeFS() != nullptr); + if (new_nca->GetStatus() == Loader::ResultStatus::Success && + new_nca->GetExeFS() != nullptr) { + LOG_INFO(Loader, " ExeFS: Update ({}) applied successfully (No Base)", + FormatTitleVersion(best_version)); + exefs = new_nca->GetExeFS(); } } } @@ -532,167 +493,44 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs } auto romfs = base_romfs; const auto& disabled = Settings::values.disabled_addons[title_id]; - bool autoloader_update_applied = false; - if (type == ContentRecordType::Program) { - VirtualDir sdmc_root = nullptr; - if (fs_controller.OpenSDMC(&sdmc_root).IsSuccess() && sdmc_root) { - const auto autoloader_updates_path = - fmt::format("autoloader/{:016X}/Updates", title_id); - const auto updates_dir = sdmc_root->GetSubdirectory(autoloader_updates_path); - if (updates_dir) { - for (const auto& mod : updates_dir->GetSubdirectories()) { - if (mod && std::find(disabled.cbegin(), disabled.cend(), mod->GetName()) == - disabled.cend()) { - for (const auto& file : mod->GetFiles()) { - if (file->GetExtension() == "nca") { - const auto new_nca = std::make_shared(file, base_nca); - if (new_nca->GetStatus() == Loader::ResultStatus::Success && - new_nca->GetType() == NCAContentType::Program && - new_nca->GetRomFS() != nullptr) { - LOG_INFO( - Loader, - " RomFS: Autoloader Update ({}) applied successfully", - mod->GetName()); - romfs = new_nca->GetRomFS(); - autoloader_update_applied = true; - break; - } - } - } - } - if (autoloader_update_applied) - break; - } + // --- NAND/External UPDATE --- + const auto active_update = GetActiveUpdate(); + u32 best_version = active_update.version; + VirtualFile best_update_raw = nullptr; + bool found_best = active_update.found; + + if (found_best) { + const auto update_tid = GetUpdateTitleID(title_id); + if (active_update.is_external) { + const auto* content_provider_union = GetUnionProvider(content_provider); + if (content_provider_union) { + best_update_raw = + content_provider_union->GetExternalEntryForVersion(update_tid, type, + best_version); } + } else { + best_update_raw = content_provider.GetEntryRaw(update_tid, type); } } - if (!autoloader_update_applied) { - const auto active_update = GetActiveUpdate(); - u32 best_version = active_update.version; - VirtualFile best_update_raw = nullptr; - bool found_best = active_update.found; - - if (found_best) { - const auto update_tid = GetUpdateTitleID(title_id); - if (active_update.is_external) { - const auto* content_provider_union = GetUnionProvider(content_provider); - if (content_provider_union) { - best_update_raw = - content_provider_union->GetExternalEntryForVersion(update_tid, type, - best_version); - } - } else { - best_update_raw = content_provider.GetEntryRaw(update_tid, type); - } - } - - if (type == ContentRecordType::Program || type == ContentRecordType::Control) { - LOG_INFO(Loader, - "PatchRomFS update lookup: title_id={:016X}, type={:02X}, found_best={}, " - "best_version={}, best_update_raw={}", - title_id, static_cast(type), found_best, best_version, - best_update_raw != nullptr); - } - - // 3. Packed Update (Fallback) - // If we still haven't found a best update, or if the packed one is enabled (named "Update" - // usually? Or "Update (PACKED)"?) The GetPatches logic names it "Update" with version - // "PACKED". - if (!found_best && - packed_update_raw != - nullptr) { // Only if no specific update found, or check strict priorities? - // Usually packed update is last resort. - const auto patch_disabled = - std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); - if (!patch_disabled) { - best_update_raw = packed_update_raw; - found_best = true; - // Version? Unknown/Packed. - } - } - - if (found_best && best_update_raw != nullptr && base_nca != nullptr) { - const auto new_nca = std::make_shared(best_update_raw, base_nca); - if (new_nca->GetStatus() == Loader::ResultStatus::Success && - new_nca->GetRomFS() != nullptr) { - LOG_INFO(Loader, " RomFS: Update ({}) applied successfully", - FormatTitleVersion(best_version)); - romfs = new_nca->GetRomFS(); - } + // 3. Packed Update (Fallback) + if (!found_best && packed_update_raw != nullptr) { + const auto patch_disabled = + std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); + if (!patch_disabled) { + best_update_raw = packed_update_raw; + found_best = true; } } - if (type == ContentRecordType::Program) { - VirtualDir sdmc_root = nullptr; - if (fs_controller.OpenSDMC(&sdmc_root).IsSuccess() && sdmc_root) { - const auto autoloader_dlc_path = fmt::format("autoloader/{:016X}/DLC", title_id); - const auto dlc_dir = sdmc_root->GetSubdirectory(autoloader_dlc_path); - if (dlc_dir) { - std::map dlc_ncas; - for (const auto& mod : dlc_dir->GetSubdirectories()) { - if (mod && std::find(disabled.cbegin(), disabled.cend(), mod->GetName()) == - disabled.cend()) { - u64 dlc_title_id = 0; - VirtualFile data_nca_file = nullptr; - - for (const auto& file : mod->GetFiles()) { - if (file->GetName().ends_with(".cnmt.nca")) { - NCA meta_nca(file); - if (meta_nca.GetStatus() == Loader::ResultStatus::Success && - !meta_nca.GetSubdirectories().empty()) { - auto section0 = meta_nca.GetSubdirectories()[0]; - if (!section0->GetFiles().empty()) { - CNMT cnmt(section0->GetFiles()[0]); - dlc_title_id = cnmt.GetTitleID(); - } - } - } else if (file->GetExtension() == "nca") { - data_nca_file = file; - } - } - - if (dlc_title_id != 0 && data_nca_file != nullptr) { - dlc_ncas[dlc_title_id] = data_nca_file; - } - } - } - - if (!dlc_ncas.empty()) { - std::vector layers; - auto base_layer = ExtractRomFS(romfs); - if (base_layer) { - layers.push_back(std::move(base_layer)); - - for (const auto& [tid, nca_file] : dlc_ncas) { - const auto dlc_nca = std::make_shared(nca_file, base_nca); - if (dlc_nca->GetStatus() == Loader::ResultStatus::Success && - dlc_nca->GetType() == NCAContentType::Data && - dlc_nca->GetRomFS() != nullptr) { - - auto extracted_dlc_romfs = ExtractRomFS(dlc_nca->GetRomFS()); - if (extracted_dlc_romfs) { - layers.push_back(std::move(extracted_dlc_romfs)); - LOG_INFO(Loader, - " RomFS: Staging Autoloader DLC TID {:016X}", tid); - } - } - } - - if (layers.size() > 1) { - auto layered_dir = - LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers)); - auto packed = CreateRomFS(std::move(layered_dir), nullptr); - if (packed) { - romfs = std::move(packed); - LOG_INFO(Loader, - " RomFS: Autoloader DLCs layered successfully."); - } - } - } - } - } + if (found_best && best_update_raw != nullptr && base_nca != nullptr) { + const auto new_nca = std::make_shared(best_update_raw, base_nca); + if (new_nca->GetStatus() == Loader::ResultStatus::Success && + new_nca->GetRomFS() != nullptr) { + LOG_INFO(Loader, " RomFS: Update ({}) applied successfully", + FormatTitleVersion(best_version)); + romfs = new_nca->GetRomFS(); } } @@ -709,7 +547,6 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { std::vector out; const auto& disabled = Settings::values.disabled_addons[title_id]; - // --- 1. Update (NAND/External) --- // --- 1. Update (NAND/External) --- // First, check for system updates (NAND/SDMC) const auto update_tid = GetUpdateTitleID(title_id); @@ -768,6 +605,15 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { // Next, check for external updates if (content_provider_union) { + // Capture the NAND update's numeric version before iterating external entries. + // The NAND entry's .version is a NACP display string ("1.4.2"), while + // canonical_version_str below is FormatTitleVersion format ("v1.4.2"). + // A string comparison alone will miss the match; compare numerically instead. + const auto nand_numeric_version = + (is_nand_control || is_nand_program) + ? content_provider.GetEntryVersion(update_tid) + : std::nullopt; + const auto updates = content_provider_union->ListExternalUpdateVersions(update_tid); for (const auto& update : updates) { // Canonical name uses the raw version number (via FormatTitleVersion) so that @@ -820,13 +666,20 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { const auto version_str = display_version_str.empty() ? canonical_version_str : display_version_str; - // Deduplicate against installed system update if versions match + // Deduplicate against installed system update if versions match. + // Primary: numeric version (robust against display string format differences). + // Fallback: string match for the FormatTitleVersion case (no-NACP). bool exists = false; - for (const auto& existing : out) { - if (existing.type == PatchType::Update && - existing.version == canonical_version_str) { - exists = true; - break; + if (nand_numeric_version && *nand_numeric_version == update.version) { + exists = true; + } + if (!exists) { + for (const auto& existing : out) { + if (existing.type == PatchType::Update && + existing.version == canonical_version_str) { + exists = true; + break; + } } } if (exists) @@ -854,80 +707,7 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { out.push_back(update_patch); } - // --- 2. Autoloader Content --- - VirtualDir sdmc_root = nullptr; - if (fs_controller.OpenSDMC(&sdmc_root).IsSuccess() && sdmc_root) { - const auto scan_autoloader_content = [&](const std::string& content_type_folder, - PatchType patch_type) { - const auto autoloader_path = - fmt::format("autoloader/{:016X}/{}", title_id, content_type_folder); - const auto content_dir = sdmc_root->GetSubdirectory(autoloader_path); - if (!content_dir) - return; - - for (const auto& mod : content_dir->GetSubdirectories()) { - if (!mod) - continue; - - std::string mod_name_str = mod->GetName(); - std::string version_str = "Unknown"; - - if (patch_type == PatchType::DLC) { - u64 dlc_title_id = 0; - for (const auto& file : mod->GetFiles()) { - if (file->GetName().ends_with(".cnmt.nca")) { - NCA meta_nca(file); - if (meta_nca.GetStatus() == Loader::ResultStatus::Success && - !meta_nca.GetSubdirectories().empty()) { - auto section0 = meta_nca.GetSubdirectories()[0]; - if (!section0->GetFiles().empty()) { - CNMT cnmt(section0->GetFiles()[0]); - dlc_title_id = cnmt.GetTitleID(); - break; - } - } - } - } - if (dlc_title_id != 0) { - version_str = fmt::format( - "{}", (dlc_title_id - GetBaseTitleID(dlc_title_id)) / 0x1000); - } else { - version_str = "DLC"; - } - } else { - for (const auto& file : mod->GetFiles()) { - if (file->GetExtension() == "nca") { - NCA nca_check(file); - if (nca_check.GetStatus() == Loader::ResultStatus::Success && - nca_check.GetType() == NCAContentType::Control) { - if (auto rfs = nca_check.GetRomFS()) { - if (auto ext = ExtractRomFS(rfs)) { - if (auto nacp_f = ext->GetFile("control.nacp")) { - NACP auto_nacp(nacp_f); - version_str = auto_nacp.GetVersionString(); - break; - } - } - } - } - } - } - } - const auto mod_disabled = - std::find(disabled.begin(), disabled.end(), mod->GetName()) != disabled.end(); - out.push_back({.enabled = !mod_disabled, - .name = mod_name_str, - .version = version_str, - .type = patch_type, - .program_id = title_id, - .title_id = title_id}); - } - }; - scan_autoloader_content("Updates", PatchType::Update); - scan_autoloader_content("DLC", PatchType::DLC); - } - - // --- 3. Citron Mods --- + // --- 2. Citron Mods --- const auto mod_dir = fs_controller.GetModificationLoadRoot(title_id); if (mod_dir != nullptr) { auto get_mod_types = [](const VirtualDir& dir) -> std::string { @@ -1201,41 +981,8 @@ std::optional PatchManager::GetGameVersion() const { PatchManager::Metadata PatchManager::GetControlMetadata() const { std::unique_ptr control_nca = nullptr; - const auto& disabled_map = Settings::values.disabled_addons; - const auto it = disabled_map.find(title_id); - const auto& disabled_for_game = - (it != disabled_map.end()) ? it->second : std::vector{}; - - VirtualDir sdmc_root = nullptr; - if (fs_controller.OpenSDMC(&sdmc_root).IsSuccess() && sdmc_root) { - const auto autoloader_updates_path = fmt::format("autoloader/{:016X}/Updates", title_id); - if (const auto autoloader_updates_dir = - sdmc_root->GetSubdirectory(autoloader_updates_path)) { - for (const auto& update_mod : autoloader_updates_dir->GetSubdirectories()) { - if (!update_mod) - continue; - if (std::find(disabled_for_game.begin(), disabled_for_game.end(), - update_mod->GetName()) == disabled_for_game.end()) { - for (const auto& file : update_mod->GetFiles()) { - if (file->GetExtension() == "nca") { - NCA nca_check(file); - if (nca_check.GetStatus() == Loader::ResultStatus::Success && - nca_check.GetType() == NCAContentType::Control) { - control_nca = std::make_unique(file); - return ParseControlNCA(*control_nca); - } - } - } - } - } - } - } const auto active_update = GetActiveUpdate(); - LOG_INFO(Loader, - "GetControlMetadata: title_id={:016X}, active_update_found={}, version={}, " - "is_external={}", - title_id, active_update.found, active_update.version, active_update.is_external); if (active_update.found) { const auto update_tid = GetUpdateTitleID(title_id); if (active_update.is_external) { @@ -1250,20 +997,14 @@ PatchManager::Metadata PatchManager::GetControlMetadata() const { } else { control_nca = content_provider.GetEntry(update_tid, ContentRecordType::Control); } - LOG_INFO(Loader, "GetControlMetadata: update control_nca={}", control_nca != nullptr); } if (control_nca == nullptr) { control_nca = content_provider.GetEntry(title_id, ContentRecordType::Control); - LOG_INFO(Loader, "GetControlMetadata: base control_nca={}", control_nca != nullptr); } - if (control_nca == nullptr) { - LOG_WARNING(Loader, "GetControlMetadata: no control NCA for title_id={:016X}", title_id); + if (control_nca == nullptr) return {}; - } - LOG_INFO(Loader, "GetControlMetadata: selected control status={}, type={:02X}", - static_cast(control_nca->GetStatus()), static_cast(control_nca->GetType())); return ParseControlNCA(*control_nca); } diff --git a/src/core/file_sys/patch_manager.h b/src/core/file_sys/patch_manager.h index f8246d73da..9c8a097488 100644 --- a/src/core/file_sys/patch_manager.h +++ b/src/core/file_sys/patch_manager.h @@ -100,7 +100,6 @@ private: }; [[nodiscard]] ActiveUpdate GetActiveUpdate() const; - [[nodiscard]] VirtualFile FindAutoloaderNCA(ContentRecordType type) const; [[nodiscard]] std::vector CollectPatches(const std::vector& patch_dirs, const std::string& build_id) const; diff --git a/src/core/file_sys/registered_cache.h b/src/core/file_sys/registered_cache.h index 952ed1eb25..63711f4119 100644 --- a/src/core/file_sys/registered_cache.h +++ b/src/core/file_sys/registered_cache.h @@ -200,9 +200,8 @@ private: std::function proc, std::function filter) const; std::vector AccumulateFiles() const; - void ProcessFiles(const std::vector& ids, std::map& out_meta, - std::map& out_meta_id) const; - void AccumulateCitronMeta(std::map& out_citron_meta) const; + void ProcessFiles(const std::vector& ids); + void AccumulateCitronMeta(); std::optional GetNcaIDFromMetadata(u64 title_id, ContentRecordType type) const; VirtualFile GetFileAtID(NcaID id) const; VirtualFile OpenFileOrDirectoryConcat(const VirtualDir& open_dir, std::string_view path) const; @@ -222,8 +221,6 @@ private: }; enum class ContentProviderUnionSlot { - Autoloader, ///< Separate functionality for multiple Updates/DLCs without being overwritten by - ///< NAND. SysNAND, ///< System NAND UserNAND, ///< User NAND SDMC, ///< SD Card diff --git a/src/core/hle/service/aoc/addon_content_manager.cpp b/src/core/hle/service/aoc/addon_content_manager.cpp index 9e56a45cd9..eab7d68997 100644 --- a/src/core/hle/service/aoc/addon_content_manager.cpp +++ b/src/core/hle/service/aoc/addon_content_manager.cpp @@ -3,7 +3,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include -#include #include #include "common/logging.h" @@ -40,40 +39,33 @@ static std::vector AccumulateAOCTitleIDs(Core::System& system) { const auto& rcu = system.GetContentProvider(); const auto list = rcu.ListEntriesFilter(FileSys::TitleType::AOC, FileSys::ContentRecordType::Data); - std::transform(list.begin(), list.end(), std::back_inserter(add_on_content), - [](const FileSys::ContentProviderEntry& rce) { return rce.title_id; }); - add_on_content.erase( - std::remove_if( - add_on_content.begin(), add_on_content.end(), - [&rcu](u64 tid) { - return rcu.GetEntry(tid, FileSys::ContentRecordType::Data)->GetStatus() != - Loader::ResultStatus::Success; - }), - add_on_content.end()); - LOG_WARNING(Service_AOC, "Accumulated {} AOC title IDs", add_on_content.size()); - for (const auto& tid : add_on_content) { - LOG_WARNING(Service_AOC, "Found AOC: {:016X}", tid); + for (const auto& entry : list) { + // Verify the NCA opens successfully + if (rcu.GetEntry(entry.title_id, FileSys::ContentRecordType::Data)->GetStatus() != + Loader::ResultStatus::Success) + continue; + + const u64 base = FileSys::GetBaseTitleID(entry.title_id); + const auto& disabled = Settings::values.disabled_addons[base]; + + // Global DLC toggle — all DLC for this title disabled + if (std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end()) + continue; + + // Per-item toggle — key matches what GetPatches emits ("DLC XXXX") + const u32 index = static_cast((entry.title_id - base) / 0x1000); + const auto item_key = fmt::format("DLC {:04d}", index); + if (std::find(disabled.begin(), disabled.end(), item_key) != disabled.end()) + continue; + + add_on_content.push_back(entry.title_id); } + + LOG_DEBUG(Service_AOC, "Accumulated {} AOC title IDs", add_on_content.size()); return add_on_content; } -static std::vector GetAOCTitleIDsForBase(const std::vector& add_on_content, u64 base) { - std::vector out; - for (const auto title_id : add_on_content) { - if (CheckAOCTitleIDMatchesBase(title_id, base)) { - out.push_back(title_id); - } - } - - std::sort(out.begin(), out.end()); - return out; -} - -static u32 GetGuestAOCIndex(std::size_t ordinal) { - return static_cast(ordinal + 1); -} - IAddOnContentManager::IAddOnContentManager(Core::System& system_) : ServiceFramework{system_, "aoc:u"}, add_on_content{AccumulateAOCTitleIDs(system)}, service_context{system_, "aoc:u"} { @@ -114,69 +106,35 @@ IAddOnContentManager::~IAddOnContentManager() { } Result IAddOnContentManager::CountAddOnContent(Out out_count, ClientProcessId process_id) { - // LOG_DEBUG(Service_AOC, "called. process_id={}", process_id.pid); - LOG_WARNING(Service_AOC, "CountAddOnContent called. process_id={}", process_id.pid); + LOG_DEBUG(Service_AOC, "called. process_id={}", process_id.pid); + const auto current = system.GetApplicationProcessProgramID(); + *out_count = static_cast( + std::count_if(add_on_content.begin(), add_on_content.end(), + [current](u64 tid) { return CheckAOCTitleIDMatchesBase(tid, current); })); - const auto& disabled = Settings::values.disabled_addons[current]; - if (std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end()) { - *out_count = 0; - R_SUCCEED(); - } - - *out_count = static_cast(GetAOCTitleIDsForBase(add_on_content, current).size()); - LOG_WARNING(Service_AOC, - "CountAddOnContent: program_id={:016X}, count={}", - current, *out_count); R_SUCCEED(); } Result IAddOnContentManager::ListAddOnContent(Out out_count, OutBuffer out_addons, u32 offset, u32 count, ClientProcessId process_id) { - LOG_WARNING(Service_AOC, "called with offset={}, count={}, process_id={}", offset, count, - process_id.pid); + LOG_DEBUG(Service_AOC, "called with offset={}, count={}, process_id={}", offset, count, + process_id.pid); const auto current = FileSys::GetBaseTitleID(system.GetApplicationProcessProgramID()); std::vector out; - const auto matching_aocs = GetAOCTitleIDsForBase(add_on_content, current); - const auto& disabled = Settings::values.disabled_addons[current]; - if (std::find(disabled.begin(), disabled.end(), "DLC") == disabled.end()) { - LOG_WARNING(Service_AOC, "Filtering AOCs for base title ID: {:016X}", current); - for (std::size_t i = 0; i < matching_aocs.size(); ++i) { - const auto guest_index = GetGuestAOCIndex(i); - LOG_WARNING(Service_AOC, - "Match! AOC {:016X} belongs to current app. Adding guest_index={}.", - matching_aocs[i], guest_index); - out.push_back(guest_index); - } - } else { - LOG_WARNING(Service_AOC, "DLCs are disabled for this title {:016X}", current); + for (u64 content_id : add_on_content) { + if (FileSys::GetBaseTitleID(content_id) == current) + out.push_back(static_cast(FileSys::GetAOCID(content_id))); } // TODO(DarkLordZach): Find the correct error code. R_UNLESS(out.size() >= offset, ResultUnknown); - const auto buffer_count = out_addons.size() / sizeof(u32); - *out_count = static_cast(std::min({out.size() - offset, static_cast(count), - buffer_count})); - LOG_WARNING(Service_AOC, - "ListAddOnContent result: base_id={:016X}, matched_total={}, offset={}, " - "requested_count={}, returned_count={}, buffer_bytes={}", - current, out.size(), offset, count, *out_count, out_addons.size()); - - for (u32 i = 0; i < *out_count; ++i) { - const auto addon_index = out[offset + i]; - const auto physical_title_id = matching_aocs[addon_index - 1]; - - LOG_WARNING(Service_AOC, - "ListAddOnContent output[{}]: addon_index={}, physical_title_id={:016X}", - i, addon_index, physical_title_id); - } - + *out_count = static_cast(std::min(out.size() - offset, count)); std::rotate(out.begin(), out.begin() + offset, out.end()); - std::memcpy(out_addons.data(), out.data(), *out_count * sizeof(u32)); R_SUCCEED(); @@ -184,17 +142,12 @@ Result IAddOnContentManager::ListAddOnContent(Out out_count, Result IAddOnContentManager::CountAddOnContentByApplicationId(Out out_count, u64 application_id) { - LOG_WARNING(Service_AOC, "called. application_id={:016X}", application_id); + LOG_DEBUG(Service_AOC, "called. application_id={:016X}", application_id); const auto current = application_id; - - const auto& disabled = Settings::values.disabled_addons[current]; - if (std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end()) { - *out_count = 0; - R_SUCCEED(); - } - - *out_count = static_cast(GetAOCTitleIDsForBase(add_on_content, current).size()); + *out_count = static_cast( + std::count_if(add_on_content.begin(), add_on_content.end(), + [current](u64 tid) { return CheckAOCTitleIDMatchesBase(tid, current); })); R_SUCCEED(); } @@ -202,48 +155,21 @@ Result IAddOnContentManager::CountAddOnContentByApplicationId(Out out_count Result IAddOnContentManager::ListAddOnContentByApplicationId( Out out_count, OutBuffer out_addons, u32 offset, u32 count, u64 application_id) { - LOG_WARNING(Service_AOC, "called with offset={}, count={}, application_id={:016X}", offset, - count, application_id); + LOG_DEBUG(Service_AOC, "called with offset={}, count={}, application_id={:016X}", offset, + count, application_id); const auto current = FileSys::GetBaseTitleID(application_id); std::vector out; - const auto matching_aocs = GetAOCTitleIDsForBase(add_on_content, current); - const auto& disabled = Settings::values.disabled_addons[current]; - if (std::find(disabled.begin(), disabled.end(), "DLC") == disabled.end()) { - LOG_WARNING(Service_AOC, "Filtering AOCs for base title ID: {:016X}", current); - for (std::size_t i = 0; i < matching_aocs.size(); ++i) { - const auto guest_index = GetGuestAOCIndex(i); - LOG_WARNING(Service_AOC, - "Match! AOC {:016X} belongs to current app. Adding guest_index={}.", - matching_aocs[i], guest_index); - out.push_back(guest_index); - } - } else { - LOG_WARNING(Service_AOC, "DLCs are disabled for this title {:016X}", current); + for (u64 content_id : add_on_content) { + if (FileSys::GetBaseTitleID(content_id) == current) + out.push_back(static_cast(FileSys::GetAOCID(content_id))); } // TODO(DarkLordZach): Find the correct error code. R_UNLESS(out.size() >= offset, ResultUnknown); - const auto buffer_count = out_addons.size() / sizeof(u32); - *out_count = static_cast(std::min({out.size() - offset, static_cast(count), - buffer_count})); - LOG_WARNING(Service_AOC, - "ListAddOnContentByApplicationId result: base_id={:016X}, matched_total={}, " - "offset={}, requested_count={}, returned_count={}, buffer_bytes={}", - current, out.size(), offset, count, *out_count, out_addons.size()); - - for (u32 i = 0; i < *out_count; ++i) { - const auto addon_index = out[offset + i]; - const auto physical_title_id = matching_aocs[addon_index - 1]; - - LOG_WARNING(Service_AOC, - "ListAddOnContentByApplicationId output[{}]: addon_index={}, " - "physical_title_id={:016X}", - i, addon_index, physical_title_id); - } - + *out_count = static_cast(std::min(out.size() - offset, count)); std::rotate(out.begin(), out.begin() + offset, out.end()); std::memcpy(out_addons.data(), out.data(), *out_count * sizeof(u32)); @@ -253,8 +179,8 @@ Result IAddOnContentManager::ListAddOnContentByApplicationId( Result IAddOnContentManager::GetAddOnContentBaseId(Out out_title_id, ClientProcessId process_id) { - // LOG_DEBUG(Service_AOC, "called. process_id={}", process_id.pid); - LOG_WARNING(Service_AOC, "GetAddOnContentBaseId called. process_id={}", process_id.pid); + LOG_DEBUG(Service_AOC, "called. process_id={}", process_id.pid); + const auto title_id = system.GetApplicationProcessProgramID(); const FileSys::PatchManager pm{title_id, system.GetFileSystemController(), system.GetContentProvider()}; @@ -262,33 +188,16 @@ Result IAddOnContentManager::GetAddOnContentBaseId(Out out_title_id, const auto res = pm.GetControlMetadata(); if (res.first == nullptr) { *out_title_id = FileSys::GetAOCBaseTitleID(title_id); - LOG_WARNING(Service_AOC, - "GetAddOnContentBaseId fallback: program_id={:016X}, aoc_base={:016X}", - title_id, *out_title_id); R_SUCCEED(); } *out_title_id = res.first->GetDLCBaseTitleId(); - LOG_WARNING(Service_AOC, - "GetAddOnContentBaseId metadata: program_id={:016X}, aoc_base={:016X}", - title_id, *out_title_id); R_SUCCEED(); } Result IAddOnContentManager::PrepareAddOnContent(s32 addon_index, ClientProcessId process_id) { - const auto program_id = system.GetApplicationProcessProgramID(); - const auto aoc_base_id = FileSys::GetAOCBaseTitleID(program_id); - const auto matching_aocs = GetAOCTitleIDsForBase(add_on_content, program_id); - u64 physical_title_id = 0; - if (addon_index > 0 && static_cast(addon_index) <= matching_aocs.size()) { - physical_title_id = matching_aocs[static_cast(addon_index) - 1]; - } - - LOG_WARNING(Service_AOC, - "PrepareAddOnContent: program_id={:016X}, aoc_base={:016X}, " - "addon_index={}, physical_title_id={:016X}, process_id={}", - program_id, aoc_base_id, addon_index, physical_title_id, + LOG_WARNING(Service_AOC, "(STUBBED) called with addon_index={}, process_id={}", addon_index, process_id.pid); R_SUCCEED(); diff --git a/src/core/loader/nca.cpp b/src/core/loader/nca.cpp index 631ccce5a7..4fad1ce8f9 100644 --- a/src/core/loader/nca.cpp +++ b/src/core/loader/nca.cpp @@ -8,6 +8,7 @@ #include "core/core.h" #include "core/file_sys/content_archive.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_factory.h" #include "core/hle/kernel/k_process.h" @@ -52,13 +53,43 @@ AppLoader_NCA::LoadResult AppLoader_NCA::Load(Kernel::KProcess& process, Core::S if (exefs == nullptr) { LOG_INFO(Loader, "No ExeFS found in NCA, looking for ExeFS from update"); - // This NCA may be a sparse base of an installed title. - // Try to fetch the ExeFS from the installed update. + // This NCA may be a sparse base of an installed title (e.g. a digital game + // whose base NCA contains no ExeFS and relies entirely on an update NCA for + // its program code). We must respect the per-game update selection stored in + // disabled_addons — using GetEntry() directly would always return the + // highest-priority installed update regardless of what the user disabled. const auto& installed = system.GetContentProvider(); - const auto update_nca = installed.GetEntry(FileSys::GetUpdateTitleID(nca->GetTitleId()), - FileSys::ContentRecordType::Program); + const auto base_title_id = nca->GetTitleId(); + const auto update_tid = FileSys::GetUpdateTitleID(base_title_id); - if (update_nca) { + FileSys::PatchManager pm{base_title_id, system.GetFileSystemController(), installed}; + const auto active_update = pm.GetActiveUpdate(); + + std::unique_ptr update_nca; + if (active_update.found) { + if (active_update.is_external) { + // External content dir — use IsUnionProvider() to safely reach the + // union API, matching the pattern used in patch_manager.cpp. + if (installed.IsUnionProvider()) { + const auto* union_provider = + static_cast(&installed); + if (const auto* ext = union_provider->GetExternalProvider()) { + if (auto raw = ext->GetEntryForVersion( + update_tid, FileSys::ContentRecordType::Program, + active_update.version)) { + update_nca = std::make_unique(raw, nca.get()); + } + } + } + } else { + // NAND / system update — GetActiveUpdate already checked disabled_addons + // and selected this as the highest enabled version. + update_nca = + installed.GetEntry(update_tid, FileSys::ContentRecordType::Program); + } + } + + if (update_nca && update_nca->GetStatus() == ResultStatus::Success) { exefs = update_nca->GetExeFS(); }