diff --git a/src/citron/configuration/configure_filesystem.cpp b/src/citron/configuration/configure_filesystem.cpp index 13634f328..b60a70ae2 100644 --- a/src/citron/configuration/configure_filesystem.cpp +++ b/src/citron/configuration/configure_filesystem.cpp @@ -33,6 +33,8 @@ ConfigureFilesystem::ConfigureFilesystem(QWidget* parent) connect(ui->gamecard_path_button, &QToolButton::pressed, this, [this] { SetDirectory(DirectoryTarget::Gamecard, ui->gamecard_path_edit); }); connect(ui->dump_path_button, &QToolButton::pressed, this, [this] { SetDirectory(DirectoryTarget::Dump, ui->dump_path_edit); }); connect(ui->load_path_button, &QToolButton::pressed, this, [this] { SetDirectory(DirectoryTarget::Load, ui->load_path_edit); }); + connect(ui->global_save_directory_button, &QToolButton::pressed, this, [this] { SetDirectory(DirectoryTarget::GlobalSave, ui->global_save_directory_edit); }); + connect(ui->global_save_directory_checkbox, &QCheckBox::checkStateChanged, this, &ConfigureFilesystem::UpdateEnabledControls); 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); @@ -65,6 +67,8 @@ void ConfigureFilesystem::SetConfiguration() { ui->gamecard_path_edit->setText(QString::fromStdString(Settings::values.gamecard_path.GetValue())); ui->dump_path_edit->setText(QString::fromStdString(Common::FS::GetCitronPathString(Common::FS::CitronPath::DumpDir))); ui->load_path_edit->setText(QString::fromStdString(Common::FS::GetCitronPathString(Common::FS::CitronPath::LoadDir))); + ui->global_save_directory_edit->setText(QString::fromStdString(Settings::values.global_custom_save_path.GetValue())); + ui->global_save_directory_checkbox->setChecked(Settings::values.global_custom_save_path_enabled.GetValue()); ui->gamecard_inserted->setChecked(Settings::values.gamecard_inserted.GetValue()); ui->gamecard_current_game->setChecked(Settings::values.gamecard_current_game.GetValue()); ui->dump_exefs->setChecked(Settings::values.dump_exefs.GetValue()); @@ -108,6 +112,28 @@ void ConfigureFilesystem::ApplyConfiguration() { // NCA Scanning Toggle UISettings::values.scan_nca = ui->scan_nca->isChecked(); + // --- GLOBAL SAVE PATH LOGIC START --- + const std::string old_path = Settings::values.global_custom_save_path.GetValue(); + const bool was_enabled = Settings::values.global_custom_save_path_enabled.GetValue(); + + const std::string new_path = ui->global_save_directory_edit->text().toStdString(); + const bool now_enabled = ui->global_save_directory_checkbox->isChecked(); + + Settings::values.global_custom_save_path = new_path; + Settings::values.global_custom_save_path_enabled = now_enabled; + + if (now_enabled && (!was_enabled || old_path != new_path)) { + QMessageBox::StandardButton reply = QMessageBox::question(this, tr("Migrate Saves to Global?"), + tr("Would you like to copy your existing saves to the new Global location?\n\n" + "This tool will prioritize your Per-Game custom saves first. If a game doesn't have a custom path, it will copy from the NAND.\n\n" + "Note: This is a COPY operation. No files will be deleted from your old directories."), + QMessageBox::Yes | QMessageBox::No); + + if (reply == QMessageBox::Yes) { + MigrateSavesToGlobal(QString::fromStdString(new_path)); + } + } + #ifdef __linux__ UISettings::values.updater_enable_backups = ui->enable_backups_checkbox->isChecked(); const bool new_custom_backup_enabled = ui->custom_backup_location_checkbox->isChecked(); @@ -168,6 +194,9 @@ void ConfigureFilesystem::SetDirectory(DirectoryTarget target, QLineEdit* edit) case DirectoryTarget::Load: caption = tr("Select Mod Load Directory..."); break; + case DirectoryTarget::GlobalSave: + caption = tr("Select Global Custom Save Directory..."); + break; } QString str; @@ -203,6 +232,8 @@ void ConfigureFilesystem::UpdateEnabledControls() { ui->gamecard_current_game->setEnabled(ui->gamecard_inserted->isChecked()); ui->gamecard_path_edit->setEnabled(ui->gamecard_inserted->isChecked() && !ui->gamecard_current_game->isChecked()); ui->gamecard_path_button->setEnabled(ui->gamecard_inserted->isChecked() && !ui->gamecard_current_game->isChecked()); + ui->global_save_directory_edit->setEnabled(ui->global_save_directory_checkbox->isChecked()); + ui->global_save_directory_button->setEnabled(ui->global_save_directory_checkbox->isChecked()); #ifdef __linux__ ui->updater_group->setVisible(true); @@ -433,3 +464,92 @@ void ConfigureFilesystem::OnRunAutoloader(bool skip_confirmation) { 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; + + // We need to find every Title ID that has a save. + // We check two places: The custom_save_paths map and the NAND user/save folder. + std::set all_program_ids; + + // 1. Get IDs from Custom Paths settings + for (const auto& [id, path] : Settings::values.custom_save_paths) { + all_program_ids.insert(id); + } + + // 2. Get IDs from NAND directory + QDir nand_save_dir(QDir(nand_root).filePath(QStringLiteral("user/save/0000000000000000"))); + for (const auto& sub_dir : nand_save_dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { + bool ok; + // The NAND folder for saves is usually the UserID, then the TitleID. + // We'll iterate the TitleID folders (formatted as 16-hex chars). + u64 tid = sub_dir.toULongLong(&ok, 16); + if (ok) all_program_ids.insert(tid); + } + + QProgressDialog progress(tr("Consolidating Saves..."), tr("Cancel"), 0, all_program_ids.size(), this); + progress.setWindowModality(Qt::WindowModal); + int current_step = 0; + + for (u64 tid : all_program_ids) { + if (progress.wasCanceled()) break; + + QString source_path; + QString tid_str = QStringLiteral("%1").arg(tid, 16, 16, QLatin1Char('0')).toUpper(); + + // LOGIC: Check if this game has a Per-Game Custom Path first. + if (Settings::values.custom_save_paths.count(tid)) { + QString custom_base = QString::fromStdString(Settings::values.custom_save_paths.at(tid)); + // Per-game paths usually point to a root where 'user/save/...' is recreated + source_path = QDir(custom_base).filePath(QStringLiteral("user/save")); + } else { + // Otherwise, use NAND as the source + source_path = QDir(nand_root).filePath(QStringLiteral("user/save")); + } + + // We only migrate if the source actually exists + if (QDir(source_path).exists()) { + QString dest_path = QDir(global_root).filePath(QStringLiteral("user/save")); + + // Perform the non-destructive copy + // We pass 0 and 0 for progress here as we are tracking progress by Title ID count instead + qint64 dummy_copied = 0; + CopyDirRecursive(source_path, dest_path, progress, dummy_copied, 0); + } + + progress.setValue(++current_step); + QCoreApplication::processEvents(); + } + + QMessageBox::information(this, tr("Consolidation Complete"), + tr("Saves have been copied to the Global directory. Your original NAND and Custom folders remain untouched.")); +} + +bool ConfigureFilesystem::CopyDirRecursive(const QString& src, const QString& dest, QProgressDialog& progress, qint64& copied, qint64 total) { + QDir src_dir(src); + if (!src_dir.exists()) return true; + + QDir().mkpath(dest); + QDirIterator it(src, QDirIterator::Subdirectories); + while (it.hasNext()) { + it.next(); + if (progress.wasCanceled()) return false; + + QFileInfo info = it.fileInfo(); + QString relative_path = src_dir.relativeFilePath(info.absoluteFilePath()); + QString dest_file_path = QDir(dest).filePath(relative_path); + + if (info.isDir()) { + QDir().mkpath(dest_file_path); + } else { + // If the file already exists at the destination, we SKIP it to be safe + // OR we overwrite if it's part of the same migration. + // To be 100% non-destructive to the SOURCE, we just use QFile::copy. + if (!QFile::exists(dest_file_path)) { + QFile::copy(info.absoluteFilePath(), dest_file_path); + } + } + } + return true; +}