diff --git a/src/citron/main.cpp b/src/citron/main.cpp index 66fb8eafa..7a53ea7ac 100644 --- a/src/citron/main.cpp +++ b/src/citron/main.cpp @@ -37,6 +37,7 @@ #include "applets/qt_profile_select.h" #include "applets/qt_software_keyboard.h" #include "applets/qt_web_browser.h" +#include "common/hex_util.h" #include "common/nvidia_flags.h" #include "common/settings_enums.h" #include "configuration/configure_input.h" @@ -45,6 +46,7 @@ #include "core/file_sys/romfs_factory.h" #include "core/file_sys/vfs/vfs.h" #include "core/file_sys/vfs/vfs_real.h" +#include "core/file_sys/nca_metadata.h" #include "core/frontend/applets/cabinet.h" #include "core/frontend/applets/controller.h" #include "core/frontend/applets/general.h" @@ -433,10 +435,13 @@ GMainWindow::GMainWindow(std::unique_ptr config_, bool has_broken_vulk show(); system->SetContentProvider(std::make_unique()); - system->RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual, - provider.get()); + system->RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual, provider.get()); system->GetFileSystemController().CreateFactories(*vfs); + autoloader_provider = std::make_unique(); + system->RegisterContentProvider(FileSys::ContentProviderUnionSlot::Autoloader, + autoloader_provider.get()); + // Remove cached contents generated during the previous session RemoveCachedContents(); @@ -1582,6 +1587,7 @@ 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_menu(ui->action_Install_With_Autoloader, &GMainWindow::OnMenuInstallWithAutoloader); connect_menu(ui->action_Trim_XCI_File, &GMainWindow::OnMenuTrimXCI); connect_menu(ui->action_Exit, &QMainWindow::close); connect_menu(ui->action_Load_Amiibo, &GMainWindow::OnLoadAmiibo); @@ -1971,6 +1977,8 @@ void GMainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletP StartGameType type) { LOG_INFO(Frontend, "citron starting..."); + 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 @@ -6129,6 +6137,260 @@ void GMainWindow::CheckForUpdatesAutomatically() { #endif } +void GMainWindow::RegisterAutoloaderContents() { + autoloader_provider->ClearAllEntries(); + const auto& disabled_addons = Settings::values.disabled_addons; + + 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; + + u64 title_id_val = 0; + try { + title_id_val = std::stoull(title_dir_entry.path().filename().string(), nullptr, 16); + } catch (const std::invalid_argument&) { + continue; + } + + const auto it = disabled_addons.find(title_id_val); + const auto& disabled_for_game = (it != disabled_addons.end()) ? it->second : std::vector{}; + + 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(); + if (std::find(disabled_for_game.begin(), disabled_for_game.end(), mod_name) != disabled_for_game.end()) { + LOG_INFO(Frontend, "Skipping disabled Autoloader content: {}", mod_name); + continue; + } + + 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::OnMenuInstallWithAutoloader() { + LOG_INFO(Loader, "AUTOLOADER: Starting Autoloader installation process."); + + const QString file_filter = tr("Nintendo Submission Package (*.nsp)"); + QStringList filenames = QFileDialog::getOpenFileNames( + this, tr("Select Update/DLC Files for Autoloader"), + QString::fromStdString(UISettings::values.roms_path), file_filter); + + if (filenames.isEmpty()) { + return; + } + + UISettings::values.roms_path = QFileInfo(filenames[0]).path().toStdString(); + + // Calculate the total size of all files to be installed for the progress dialog. + 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 to Autoloader..."), 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, "AUTOLOADER: Processing sanitized file path: {}", file_path); + + auto vfs_file = vfs->OpenFile(file_path, FileSys::OpenMode::Read); + if (!vfs_file) { + LOG_ERROR(Loader, "AUTOLOADER: 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, "AUTOLOADER: 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, "AUTOLOADER: 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, "AUTOLOADER: 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 = (cnmt.GetType() == FileSys::TitleType::Update) ? "Updates" : "DLC"; + 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 = vfs->CreateDirectory(dest_path_str, FileSys::OpenMode::ReadWrite); + if (!dest_dir) { + LOG_ERROR(Loader, "AUTOLOADER: 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->Resize(source_file->GetSize())) { + LOG_ERROR(Loader, "AUTOLOADER: 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, "AUTOLOADER: 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("Autoloader 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(); }