mirror of
https://git.citron-emu.org/citron/emulator
synced 2025-12-20 11:03:56 +00:00
feat(fs): Implement Autoloader (W.I.P.)
This commit is contained in:
@@ -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<QtConfig> config_, bool has_broken_vulk
|
||||
show();
|
||||
|
||||
system->SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
|
||||
system->RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual,
|
||||
provider.get());
|
||||
system->RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual, provider.get());
|
||||
system->GetFileSystemController().CreateFactories(*vfs);
|
||||
|
||||
autoloader_provider = std::make_unique<FileSys::ManualContentProvider>();
|
||||
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<u64>(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<std::string>{};
|
||||
|
||||
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<FileSys::CNMT> 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<u8> 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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user