Merge pull request 'feat(fix): Re-structure Pathing Logic for necessary additional configuration for multiple different varieties' (#110) from fix/restructure-save-pathing-logic into main

Reviewed-on: https://git.citron-emu.org/Citron/Emulator/pulls/110
This commit is contained in:
Collecting
2026-01-24 03:01:36 +01:00
4 changed files with 184 additions and 109 deletions

View File

@@ -959,8 +959,13 @@ void GameList::DonePopulating(const QStringList& watch_list) {
// Only sync if we aren't rebuilding the UI and the game isn't running. // Only sync if we aren't rebuilding the UI and the game isn't running.
if (main_window && !main_window->IsConfiguring() && !system.IsPoweredOn()) { if (main_window && !main_window->IsConfiguring() && !system.IsPoweredOn()) {
LOG_INFO(Frontend, "Game List populated. Triggering Mirror Sync..."); if (!main_window->HasPerformedInitialSync()) {
system.GetFileSystemController().GetSaveDataFactory().PerformStartupMirrorSync(); LOG_INFO(Frontend, "Mirroring: Performing one-time startup sync...");
system.GetFileSystemController().GetSaveDataFactory().PerformStartupMirrorSync();
main_window->SetPerformedInitialSync(true);
} else {
LOG_INFO(Frontend, "Mirroring: Startup sync already performed this session. Skipping.");
}
} else { } else {
LOG_INFO(Frontend, "Mirroring: Startup sync skipped (Reason: UI Busy or Game is Emulating)."); LOG_INFO(Frontend, "Mirroring: Startup sync skipped (Reason: UI Busy or Game is Emulating).");
} }
@@ -1014,14 +1019,20 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
} }
void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path, const QString& game_name) { void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path, const QString& game_name) {
const bool is_mirrored = Settings::values.mirrored_save_paths.count(program_id);
const bool has_custom_path = Settings::values.custom_save_paths.count(program_id);
QString mirror_base_path;
QAction* favorite = context_menu.addAction(tr("Favorite")); QAction* favorite = context_menu.addAction(tr("Favorite"));
context_menu.addSeparator(); context_menu.addSeparator();
QAction* start_game = context_menu.addAction(tr("Start Game")); QAction* start_game = context_menu.addAction(tr("Start Game"));
QAction* start_game_global = context_menu.addAction(tr("Start Game without Custom Configuration")); QAction* start_game_global = context_menu.addAction(tr("Start Game without Custom Configuration"));
context_menu.addSeparator(); context_menu.addSeparator();
QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location")); QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location"));
QAction* open_nand_location = context_menu.addAction(tr("Open NAND Location"));
QAction* set_custom_save_path = context_menu.addAction(tr("Set Custom Save Path")); QAction* set_custom_save_path = context_menu.addAction(tr("Set Custom Save Path"));
QAction* remove_custom_save_path = context_menu.addAction(tr("Revert to NAND Save Path")); QAction* remove_custom_save_path = context_menu.addAction(tr("Revert to NAND Save Path"));
QAction* disable_mirroring = context_menu.addAction(tr("Disable Mirroring"));
QAction* open_mod_location = context_menu.addAction(tr("Open Mod Data Location")); QAction* open_mod_location = context_menu.addAction(tr("Open Mod Data Location"));
QMenu* open_sdmc_mod_menu = context_menu.addMenu(tr("Open SDMC Mod Data Location")); QMenu* open_sdmc_mod_menu = context_menu.addMenu(tr("Open SDMC Mod Data Location"));
QAction* open_current_game_sdmc = open_sdmc_mod_menu->addAction(tr("Open Current Game Location")); QAction* open_current_game_sdmc = open_sdmc_mod_menu->addAction(tr("Open Current Game Location"));
@@ -1053,14 +1064,15 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
context_menu.addSeparator(); context_menu.addSeparator();
QAction* properties = context_menu.addAction(tr("Properties")); QAction* properties = context_menu.addAction(tr("Properties"));
const bool has_custom_path = Settings::values.custom_save_paths.count(program_id);
favorite->setVisible(program_id != 0); favorite->setVisible(program_id != 0);
favorite->setCheckable(true); favorite->setCheckable(true);
favorite->setChecked(UISettings::values.favorited_ids.contains(program_id)); favorite->setChecked(UISettings::values.favorited_ids.contains(program_id));
open_save_location->setVisible(program_id != 0); open_save_location->setVisible(program_id != 0);
set_custom_save_path->setVisible(program_id != 0); open_nand_location->setVisible(is_mirrored);
open_nand_location->setToolTip(tr("Citron uses your NAND while syncing. If you need to make save data modifications, do so in here."));
set_custom_save_path->setVisible(program_id != 0 && !is_mirrored);
remove_custom_save_path->setVisible(program_id != 0 && has_custom_path); remove_custom_save_path->setVisible(program_id != 0 && has_custom_path);
disable_mirroring->setVisible(is_mirrored);
open_mod_location->setVisible(program_id != 0); open_mod_location->setVisible(program_id != 0);
open_sdmc_mod_menu->menuAction()->setVisible(program_id != 0); open_sdmc_mod_menu->menuAction()->setVisible(program_id != 0);
open_transferable_shader_cache->setVisible(program_id != 0); open_transferable_shader_cache->setVisible(program_id != 0);
@@ -1071,6 +1083,39 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
remove_shader_cache->setVisible(program_id != 0); remove_shader_cache->setVisible(program_id != 0);
remove_all_content->setVisible(program_id != 0); remove_all_content->setVisible(program_id != 0);
if (is_mirrored) {
const bool has_global_path = Settings::values.global_custom_save_path_enabled.GetValue() &&
!Settings::values.global_custom_save_path.GetValue().empty();
if (has_global_path) {
open_nand_location->setText(tr("Open Global Save Path Location"));
open_nand_location->setToolTip(tr("The global save path is being used as the base for save data mirroring."));
mirror_base_path = QString::fromStdString(Settings::values.global_custom_save_path.GetValue());
} else {
// Text is already "Open NAND Location", so we just set the correct path and a more descriptive tooltip.
open_nand_location->setToolTip(tr("Citron's default NAND is being used as the base for save data mirroring."));
mirror_base_path = QString::fromStdString(Common::FS::GetCitronPathString(Common::FS::CitronPath::NANDDir));
}
connect(open_nand_location, &QAction::triggered, [this, program_id, mirror_base_path]() {
const auto user_id = system.GetProfileManager().GetLastOpenedUser().AsU128();
// This constructs the relative path to the specific game's save folder
const std::string relative_save_path = fmt::format("user/save/{:016X}/{:016X}{:016X}/{:016X}", 0, user_id[1], user_id[0], program_id);
// Combine the determined base path (Global or default NAND) with the relative game path
const auto full_save_path = std::filesystem::path(mirror_base_path.toStdString()) / relative_save_path;
// Ensure the parent directory exists before trying to open it
if (!std::filesystem::exists(full_save_path.parent_path())) {
std::filesystem::create_directories(full_save_path.parent_path());
}
QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(full_save_path.string())));
});
}
submit_compat_report->setToolTip(tr("Requires GitHub account."));
connect(favorite, &QAction::triggered, [this, program_id]() { ToggleFavorite(program_id); }); connect(favorite, &QAction::triggered, [this, program_id]() { ToggleFavorite(program_id); });
connect(open_save_location, &QAction::triggered, [this, program_id, path]() { emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData, path); }); connect(open_save_location, &QAction::triggered, [this, program_id, path]() { emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData, path); });
@@ -1128,87 +1173,109 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
return true; return true;
}; };
connect(set_custom_save_path, &QAction::triggered, [this, program_id, copyWithProgress]() { connect(set_custom_save_path, &QAction::triggered, [this, program_id, copyWithProgress]() {
const QString new_path = QFileDialog::getExistingDirectory(this, tr("Select Custom Save Data Location")); const QString new_path = QFileDialog::getExistingDirectory(this, tr("Select Custom Save Data Location"));
if (new_path.isEmpty()) return; if (new_path.isEmpty()) return;
const auto nand_dir_str = Common::FS::GetCitronPathString(Common::FS::CitronPath::NANDDir); std::string base_save_path_str;
const QString nand_dir = QString::fromStdString(nand_dir_str); if (Settings::values.global_custom_save_path_enabled.GetValue() &&
const auto user_id = system.GetProfileManager().GetLastOpenedUser().AsU128(); !Settings::values.global_custom_save_path.GetValue().empty()) {
const std::string relative_save_path = fmt::format("user/save/{:016X}/{:016X}{:016X}/{:016X}", 0, user_id[1], user_id[0], program_id); base_save_path_str = Settings::values.global_custom_save_path.GetValue();
const QString citron_nand_save_path = QDir(nand_dir).filePath(QString::fromStdString(relative_save_path)); } else {
base_save_path_str = Common::FS::GetCitronPathString(Common::FS::CitronPath::NANDDir);
}
const QString base_dir = QString::fromStdString(base_save_path_str);
bool mirroring_enabled = false; const auto user_id = system.GetProfileManager().GetLastOpenedUser().AsU128();
QString detected_emu = GetDetectedEmulatorName(new_path, program_id, nand_dir); const std::string relative_save_path = fmt::format("user/save/{:016X}/{:016X}{:016X}/{:016X}", 0, user_id[1], user_id[0], program_id);
if (!detected_emu.isEmpty()) { // This path points to the save data within either the Global Path or the NAND.
QMessageBox::StandardButton mirror_reply = QMessageBox::question(this, tr("Enable Save Mirroring?"), const QString internal_save_path = QDir(base_dir).filePath(QString::fromStdString(relative_save_path));
tr("Citron has detected a %1 save structure.\n\n"
"Would you like to enable 'Intelligent Mirroring'? This will pull the data into Citron's NAND "
"and keep both locations synced whenever you play. A backup of what is inside of your NAND for Citron will be backed up for you with a corresponding folder name, so if you'd prefer to use Citron's data, please go to that folder & copy the contents and paste it back into the regular Title ID directory. BE WARNED: Please do not A. Have both emulators open during this process, and B. Ensure you do not fully 'delete' your backup that was provided to you incase something goes wrong.").arg(detected_emu),
QMessageBox::Yes | QMessageBox::No);
if (mirror_reply == QMessageBox::Yes) { bool mirroring_enabled = false;
mirroring_enabled = true; // The check for other emulators uses the determined base directory.
} QString detected_emu = GetDetectedEmulatorName(new_path, program_id, base_dir);
}
if (!detected_emu.isEmpty()) {
QDir citron_dir(citron_nand_save_path); QMessageBox::StandardButton mirror_reply = QMessageBox::question(this, tr("Enable Save Mirroring?"),
if (citron_dir.exists() && !citron_dir.isEmpty()) { tr("Citron has detected a %1 save structure.\n\n"
if (mirroring_enabled) { "Would you like to enable 'Intelligent Mirroring'? This will pull the data into Citron's internal save directory "
// Non-destructive backup for mirroring "(currently set to '%2') and keep both locations synced whenever you play. A backup of your existing Citron data "
QString timestamp = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd_hh-mm-ss")); "will be created. BE WARNED: Please do not have both emulators open during this process.").arg(detected_emu, base_dir),
QString backup_path = citron_nand_save_path + QStringLiteral("_mirror_backup_") + timestamp; QMessageBox::Yes | QMessageBox::No);
// Ensure parent directory exists before renaming if (mirror_reply == QMessageBox::Yes) {
QDir().mkpath(QFileInfo(backup_path).absolutePath()); mirroring_enabled = true;
if (QDir().rename(citron_nand_save_path, backup_path)) {
LOG_INFO(Frontend, "Safety: Existing NAND data moved to backup: {}", backup_path.toStdString());
}
} else {
// Standard Citron behavior for manual paths (Override mode)
QMessageBox::StandardButton reply = QMessageBox::question(this, tr("Move Save Data"),
tr("You have existing save data in the NAND. Would you like to move it to the new custom save path?"),
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
if (reply == QMessageBox::Cancel) return;
if (reply == QMessageBox::Yes) {
// In override mode, we move files TO the new path
const QString full_dest_path = QDir(new_path).filePath(QString::fromStdString(relative_save_path));
if (copyWithProgress(citron_nand_save_path, full_dest_path, this)) {
QDir(citron_nand_save_path).removeRecursively();
QMessageBox::information(this, tr("Success"), tr("Successfully moved save data to the new location."));
} else {
QMessageBox::warning(this, tr("Error"), tr("Failed to move save data. Please see the log for more details."));
}
}
}
} }
}
QDir internal_dir(internal_save_path);
if (internal_dir.exists() && !internal_dir.isEmpty()) {
if (mirroring_enabled) { if (mirroring_enabled) {
// Initial Pull (External -> Citron NAND) // Non-destructive backup for mirroring, now created in the base directory.
// We copy FROM the selected folder TO the Citron NAND location QString timestamp = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd_hh-mm-ss"));
if (copyWithProgress(new_path, citron_nand_save_path, this)) { QString backup_path = internal_save_path + QStringLiteral("_mirror_backup_") + timestamp;
// IMPORTANT: Save to the NEW mirror map
Settings::values.mirrored_save_paths.insert_or_assign(program_id, new_path.toStdString());
// CLEAR the standard custom path so the emulator boots from NAND
Settings::values.custom_save_paths.erase(program_id);
QMessageBox::information(this, tr("Success"), tr("Mirroring established. Your data has been pulled into the Citron NAND.")); // Ensure parent directory exists before renaming
} else { QDir().mkpath(QFileInfo(backup_path).absolutePath());
QMessageBox::warning(this, tr("Error"), tr("Failed to pull data from the mirror source."));
return; if (QDir().rename(internal_save_path, backup_path)) {
LOG_INFO(Frontend, "Safety: Existing internal data moved to backup: {}", backup_path.toStdString());
} }
} else { } else {
// Standard Path Override // Standard Citron behavior for manual paths (Override mode)
Settings::values.custom_save_paths.insert_or_assign(program_id, new_path.toStdString()); QMessageBox::StandardButton reply = QMessageBox::question(this, tr("Move Save Data"),
// Remove from mirror map if it was there before tr("You have existing save data in your internal save directory. Would you like to move it to the new custom save path?"),
Settings::values.mirrored_save_paths.erase(program_id); QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
}
emit SaveConfig(); if (reply == QMessageBox::Cancel) return;
if (reply == QMessageBox::Yes) {
// In override mode, we move files TO the new path
const QString full_dest_path = QDir(new_path).filePath(QString::fromStdString(relative_save_path));
if (copyWithProgress(internal_save_path, full_dest_path, this)) {
QDir(internal_save_path).removeRecursively();
QMessageBox::information(this, tr("Success"), tr("Successfully moved save data to the new location."));
} else {
QMessageBox::warning(this, tr("Error"), tr("Failed to move save data. Please see the log for more details."));
}
}
}
}
if (mirroring_enabled) {
// Initial Pull (External -> Internal)
// We copy FROM the selected folder TO the correct internal save location.
if (copyWithProgress(new_path, internal_save_path, this)) {
// IMPORTANT: Save to the NEW mirror map
Settings::values.mirrored_save_paths.insert_or_assign(program_id, new_path.toStdString());
// CLEAR the standard custom path so the emulator boots from the internal directory
Settings::values.custom_save_paths.erase(program_id);
QMessageBox::information(this, tr("Success"), tr("Mirroring established. Your data has been pulled into the internal Citron save directory."));
} else {
QMessageBox::warning(this, tr("Error"), tr("Failed to pull data from the mirror source."));
return;
}
} else {
// Standard Path Override
Settings::values.custom_save_paths.insert_or_assign(program_id, new_path.toStdString());
// Remove from mirror map if it was there before
Settings::values.mirrored_save_paths.erase(program_id);
}
emit SaveConfig();
});
connect(disable_mirroring, &QAction::triggered, [this, program_id]() {
if (QMessageBox::question(this, tr("Disable Mirroring"),
tr("Are you sure you want to disable mirroring for this game?\n\nThe directories will no longer be synced."),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
Settings::values.mirrored_save_paths.erase(program_id);
emit SaveConfig();
QMessageBox::information(this, tr("Mirroring Disabled"),
tr("Mirroring has been disabled for this game. It will now use the save data from the NAND."));
}
}); });
connect(open_current_game_sdmc, &QAction::triggered, [program_id]() { connect(open_current_game_sdmc, &QAction::triggered, [program_id]() {

View File

@@ -2308,6 +2308,13 @@ void GMainWindow::OnEmulationStopped() {
emulation_running = false; emulation_running = false;
// Reset the startup sync flag for the next session.
has_performed_initial_sync = false;
LOG_INFO(Frontend, "Mirroring: Emulation stopped. Re-arming startup sync for next game list refresh.");
// This is necessary to reset the in-memory state for the next launch.
system->GetFileSystemController().CreateFactories(*vfs, true);
discord_rpc->Update(); discord_rpc->Update();
#ifdef __unix__ #ifdef __unix__
@@ -2426,30 +2433,28 @@ void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target
case GameListOpenTarget::SaveData: { case GameListOpenTarget::SaveData: {
open_target = tr("Save Data"); open_target = tr("Save Data");
// 1. Check for Mirrored Path FIRST (opens the external directory) // 1. Priority 1: Mirrored Path (opens the external directory)
if (Settings::values.mirrored_save_paths.count(program_id)) { if (Settings::values.mirrored_save_paths.count(program_id)) {
const std::string& mirrored_path_str = const std::string& mirrored_path_str =
Settings::values.mirrored_save_paths.at(program_id); Settings::values.mirrored_save_paths.at(program_id);
if (!mirrored_path_str.empty() && Common::FS::IsDir(mirrored_path_str)) { if (!mirrored_path_str.empty() && Common::FS::IsDir(mirrored_path_str)) {
LOG_INFO(Frontend, "Opening mirrored save data path for program_id={:016x}", LOG_INFO(Frontend, "Opening external mirrored save data path for program_id={:016x}",
program_id); program_id);
QDesktopServices::openUrl( QDesktopServices::openUrl(
QUrl::fromLocalFile(QString::fromStdString(mirrored_path_str))); QUrl::fromLocalFile(QString::fromStdString(mirrored_path_str)));
return; return;
} }
} }
// 2. Check for Per-Game Custom Path // 2. Priority 2: Per-Game Custom Path
else if (Settings::values.custom_save_paths.count(program_id)) { else if (Settings::values.custom_save_paths.count(program_id)) {
const std::string& custom_path_str = Settings::values.custom_save_paths.at(program_id); const std::string& custom_path_str = Settings::values.custom_save_paths.at(program_id);
if (!custom_path_str.empty() && Common::FS::IsDir(custom_path_str)) { if (!custom_path_str.empty() && Common::FS::IsDir(custom_path_str)) {
LOG_INFO(Frontend, "Opening custom save data path for program_id={:016x}", LOG_INFO(Frontend, "Opening per-game custom save data path for program_id={:016x}", program_id);
program_id); QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(custom_path_str)));
QDesktopServices::openUrl(
QUrl::fromLocalFile(QString::fromStdString(custom_path_str)));
return; return;
} }
} }
// 3. Check for Global Custom Save Path // 3. Priority 3: Global Custom Path
else if (Settings::values.global_custom_save_path_enabled.GetValue()) { else if (Settings::values.global_custom_save_path_enabled.GetValue()) {
const std::string& global_path_str = const std::string& global_path_str =
Settings::values.global_custom_save_path.GetValue(); Settings::values.global_custom_save_path.GetValue();

View File

@@ -118,6 +118,8 @@ public:
void RefreshGameList(); void RefreshGameList();
GRenderWindow* GetRenderWindow() const { return render_window; } GRenderWindow* GetRenderWindow() const { return render_window; }
bool ExtractZipToDirectoryPublic(const std::filesystem::path& zip_path, const std::filesystem::path& extract_path); bool ExtractZipToDirectoryPublic(const std::filesystem::path& zip_path, const std::filesystem::path& extract_path);
[[nodiscard]] bool HasPerformedInitialSync() const { return has_performed_initial_sync; }
void SetPerformedInitialSync(bool synced) { has_performed_initial_sync = synced; }
signals: signals:
void EmulationStarting(EmuThread* emu_thread); void EmulationStarting(EmuThread* emu_thread);
void EmulationStopping(); void EmulationStopping();
@@ -403,6 +405,7 @@ private:
bool is_tas_recording_dialog_active{}; bool is_tas_recording_dialog_active{};
bool m_is_updating_theme = false; bool m_is_updating_theme = false;
bool m_is_configuring = false; bool m_is_configuring = false;
bool has_performed_initial_sync = false;
#ifdef __unix__ #ifdef __unix__
QSocketNotifier* sig_interrupt_notifier; QSocketNotifier* sig_interrupt_notifier;
static std::array<int, 3> sig_interrupt_fds; static std::array<int, 3> sig_interrupt_fds;

View File

@@ -422,51 +422,51 @@ std::shared_ptr<FileSys::SaveDataFactory> FileSystemController::CreateSaveDataFa
const auto rw_mode = FileSys::OpenMode::ReadWrite; const auto rw_mode = FileSys::OpenMode::ReadWrite;
auto vfs = system.GetFilesystem(); auto vfs = system.GetFilesystem();
// 1. Priority 1: Mirrored Path Override (Forces NAND) // 1. Determine the correct BASE directory FIRST.
// The base directory is either the Global Custom Save Path or the default NAND.
std::string base_save_path_str;
if (Settings::values.global_custom_save_path_enabled.GetValue() &&
!Settings::values.global_custom_save_path.GetValue().empty()) {
base_save_path_str = Settings::values.global_custom_save_path.GetValue();
LOG_INFO(Service_FS, "Save Path: Using Global Custom Save Path as the base: {}", base_save_path_str);
} else {
base_save_path_str = Common::FS::GetCitronPathString(CitronPath::NANDDir);
LOG_INFO(Service_FS, "Save Path: Using default NAND as the base.");
}
auto base_directory = vfs->OpenDirectory(base_save_path_str, rw_mode);
// 2. Check for Mirroring.
if (Settings::values.mirrored_save_paths.count(program_id)) { if (Settings::values.mirrored_save_paths.count(program_id)) {
LOG_INFO(Service_FS, LOG_INFO(Service_FS,
"Save Path: Mirroring detected for Program ID {:016X}. Forcing use of NAND " "Save Path: Mirroring detected for Program ID {:016X}. Syncing against the determined base directory.",
"directory for syncing.",
program_id); program_id);
const auto nand_directory =
vfs->OpenDirectory(Common::FS::GetCitronPathString(CitronPath::NANDDir), rw_mode);
return std::make_shared<FileSys::SaveDataFactory>(system, program_id, return std::make_shared<FileSys::SaveDataFactory>(system, program_id,
std::move(nand_directory)); std::move(base_directory));
} }
std::string custom_path_str; // 3. Check for Per-Game Custom Path override.
// 2. Priority 2: Individual Game Override
if (Settings::values.custom_save_paths.count(program_id)) { if (Settings::values.custom_save_paths.count(program_id)) {
custom_path_str = Settings::values.custom_save_paths.at(program_id); const std::string custom_path_str = Settings::values.custom_save_paths.at(program_id);
LOG_INFO(Service_FS, "Save Path: Using Per-Game Custom Path for Program ID {:016X}: {}", LOG_INFO(Service_FS, "Save Path: Using Per-Game Custom Path for Program ID {:016X}: {}",
program_id, custom_path_str); program_id, custom_path_str);
}
// 3. Priority 3: Global Override
else if (Settings::values.global_custom_save_path_enabled.GetValue()) {
custom_path_str = Settings::values.global_custom_save_path.GetValue();
LOG_INFO(Service_FS, "Save Path: Using Global Custom Save Path: {}", custom_path_str);
}
// If any custom logic is hit, use that path but KEEP NAND as backup target
if (!custom_path_str.empty()) {
const std::filesystem::path custom_path = custom_path_str; const std::filesystem::path custom_path = custom_path_str;
if (Common::FS::IsDir(custom_path)) { if (Common::FS::IsDir(custom_path)) {
auto custom_save_directory = vfs->OpenDirectory(custom_path_str, rw_mode); auto custom_save_directory = vfs->OpenDirectory(custom_path_str, rw_mode);
auto nand_directory =
vfs->OpenDirectory(Common::FS::GetCitronPathString(CitronPath::NANDDir), rw_mode);
// The base_directory (Global Path or NAND) is now correctly passed as the backup.
return std::make_shared<FileSys::SaveDataFactory>( return std::make_shared<FileSys::SaveDataFactory>(
system, program_id, std::move(custom_save_directory), std::move(nand_directory)); system, program_id, std::move(custom_save_directory), std::move(base_directory));
} }
} }
// 4. Fallback: Standard NAND // 4. Fallback: If no mirroring and no per-game path, use the determined base directory.
LOG_INFO(Service_FS, "Save Path: No custom paths found. Falling back to default NAND."); LOG_INFO(Service_FS, "Save Path: No overrides found. Using the determined base directory.");
const auto nand_directory =
vfs->OpenDirectory(Common::FS::GetCitronPathString(CitronPath::NANDDir), rw_mode);
return std::make_shared<FileSys::SaveDataFactory>(system, program_id, return std::make_shared<FileSys::SaveDataFactory>(system, program_id,
std::move(nand_directory)); std::move(base_directory));
} }
Result FileSystemController::OpenSDMC(FileSys::VirtualDir* out_sdmc) const { Result FileSystemController::OpenSDMC(FileSys::VirtualDir* out_sdmc) const {