From cd3c5e1d8864216e3627d5bd8e8d5c8541b9f570 Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 22 Dec 2025 08:32:57 +0000 Subject: [PATCH 01/13] fs(feat): Add Backup Saves for Custom Save Paths Signed-off-by: Collecting --- src/common/settings.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/common/settings.h b/src/common/settings.h index 567ee6b3e..cb7a1df37 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -684,8 +684,9 @@ struct Values { // Key: build_id (hex string), Value: set of disabled cheat names std::map> disabled_cheats; - // Custom Save Paths + // Custom Save Paths (with backups) std::map custom_save_paths; + Setting backup_saves_to_nand{linkage, false, "backup_saves_to_nand", Category::DataStorage}; }; extern Values values; From 693cb6dea0a06e498a809c359bb3a6046e622151 Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 22 Dec 2025 08:33:58 +0000 Subject: [PATCH 02/13] fs(feat): Add Backup Saves for Custom Save Paths Signed-off-by: Collecting --- src/citron/configuration/configure_filesystem.ui | 1 + 1 file changed, 1 insertion(+) diff --git a/src/citron/configuration/configure_filesystem.ui b/src/citron/configuration/configure_filesystem.ui index 18d085c9a..be236c794 100644 --- a/src/citron/configuration/configure_filesystem.ui +++ b/src/citron/configuration/configure_filesystem.ui @@ -87,6 +87,7 @@ Cache Game List Metadata Reset Metadata Cache Scan for .nca files (Advanced: Significantly slows down scanning) + Allow Backup Saves to NAND if using Custom Save Path From d21d5cd0e073c178b88e581a3117bda32df4786a Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 22 Dec 2025 08:34:26 +0000 Subject: [PATCH 03/13] fs(feat): Add Backup Saves for Custom Save Paths Signed-off-by: Collecting --- src/citron/configuration/configure_filesystem.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/citron/configuration/configure_filesystem.cpp b/src/citron/configuration/configure_filesystem.cpp index bab26c910..13634f328 100644 --- a/src/citron/configuration/configure_filesystem.cpp +++ b/src/citron/configuration/configure_filesystem.cpp @@ -71,6 +71,7 @@ void ConfigureFilesystem::SetConfiguration() { 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 ui->scan_nca->setChecked(UISettings::values.scan_nca.GetValue()); @@ -102,6 +103,7 @@ void ConfigureFilesystem::ApplyConfiguration() { 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 UISettings::values.scan_nca = ui->scan_nca->isChecked(); From ac354a6d280eb8340225a3b25239390013d832c8 Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 22 Dec 2025 08:34:53 +0000 Subject: [PATCH 04/13] fs(feat): Add Backup Saves for Custom Save Paths Signed-off-by: Collecting --- src/frontend_common/config.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/frontend_common/config.cpp b/src/frontend_common/config.cpp index 6148d3310..8b9b85ba3 100644 --- a/src/frontend_common/config.cpp +++ b/src/frontend_common/config.cpp @@ -283,6 +283,7 @@ void Config::ReadDataStorageValues() { FS::SetCitronPath(FS::CitronPath::TASDir, ReadStringSetting(std::string("tas_directory"))); ReadCategory(Settings::Category::DataStorage); + Settings::values.backup_saves_to_nand = ReadBooleanSetting(std::string("backup_saves_to_nand"), false); EndGroup(); } @@ -636,6 +637,7 @@ void Config::SaveDataStorageValues() { std::make_optional(FS::GetCitronPathString(FS::CitronPath::TASDir))); WriteCategory(Settings::Category::DataStorage); + WriteBooleanSetting(std::string("backup_saves_to_nand"), Settings::values.backup_saves_to_nand.GetValue()); EndGroup(); } From f4d712d497fe7951a82ac9dbc2f96d950a646469 Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 22 Dec 2025 08:35:57 +0000 Subject: [PATCH 05/13] fs(feat): Add Backup Saves for Custom Save Paths Signed-off-by: Collecting --- .../directory_save_data_filesystem.cpp | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/core/file_sys/directory_save_data_filesystem.cpp b/src/core/file_sys/directory_save_data_filesystem.cpp index 402025d56..cdd7ce72e 100644 --- a/src/core/file_sys/directory_save_data_filesystem.cpp +++ b/src/core/file_sys/directory_save_data_filesystem.cpp @@ -4,6 +4,7 @@ #include #include #include "common/logging/log.h" +#include "common/settings.h" #include "core/file_sys/errors.h" #include "core/file_sys/directory_save_data_filesystem.h" @@ -16,8 +17,9 @@ constexpr int RetryWaitTimeMs = 100; } // Anonymous namespace -DirectorySaveDataFileSystem::DirectorySaveDataFileSystem(VirtualDir base_filesystem) - : base_fs(std::move(base_filesystem)), extra_data_accessor(base_fs), journaling_enabled(true), +DirectorySaveDataFileSystem::DirectorySaveDataFileSystem(VirtualDir base_filesystem, VirtualDir backup_filesystem) + : base_fs(std::move(base_filesystem)), backup_fs(std::move(backup_filesystem)), + extra_data_accessor(base_fs), journaling_enabled(true), open_writable_files(0) {} DirectorySaveDataFileSystem::~DirectorySaveDataFileSystem() = default; @@ -126,6 +128,25 @@ Result DirectorySaveDataFileSystem::Commit() { // Update cached committed_dir reference committed_dir = base_fs->GetSubdirectory(CommittedDirectoryName); + if (Settings::values.backup_saves_to_nand.GetValue() && backup_fs != nullptr) { + LOG_INFO(Service_FS, "Dual-Save: Backing up custom save to NAND..."); + + // 1. Find or Create the '0' (Committed) folder in the NAND + auto nand_committed = backup_fs->GetSubdirectory(CommittedDirectoryName); + if (nand_committed == nullptr) { + nand_committed = backup_fs->CreateSubdirectory(CommittedDirectoryName); + } + + if (nand_committed != nullptr) { + // 2. Wipe whatever old backup was there + backup_fs->DeleteSubdirectoryRecursive(CommittedDirectoryName); + nand_committed = backup_fs->CreateSubdirectory(CommittedDirectoryName); + + // 3. Copy the fresh data from our 'working' area to the NAND '0' folder + CopyDirectoryRecursively(nand_committed, working_dir); + } + } + LOG_INFO(Service_FS, "Save data committed successfully"); return ResultSuccess; } From ddddb28c549c02ad8042eda5b5748824c4046e12 Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 22 Dec 2025 08:36:49 +0000 Subject: [PATCH 06/13] fs(feat): Add Backup Saves for Custom Save Paths Signed-off-by: Collecting --- src/core/file_sys/directory_save_data_filesystem.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/file_sys/directory_save_data_filesystem.h b/src/core/file_sys/directory_save_data_filesystem.h index eedcddc6e..954841cdd 100644 --- a/src/core/file_sys/directory_save_data_filesystem.h +++ b/src/core/file_sys/directory_save_data_filesystem.h @@ -18,7 +18,8 @@ namespace FileSys { /// Uses /0 (committed) and /1 (working) directories for journaling class DirectorySaveDataFileSystem { public: - explicit DirectorySaveDataFileSystem(VirtualDir base_filesystem); + // optional directory here for backup + explicit DirectorySaveDataFileSystem(VirtualDir base_filesystem, VirtualDir backup_filesystem = nullptr); ~DirectorySaveDataFileSystem(); /// Initialize the journaling filesystem @@ -54,6 +55,7 @@ private: Result RetryFinitelyForTargetLocked(std::function operation); VirtualDir base_fs; + VirtualDir backup_fs; // This will store the NAND path VirtualDir working_dir; VirtualDir committed_dir; SaveDataExtraDataAccessor extra_data_accessor; From 67bf3b53b70c2aa66b0f7a091b4fea60d7315083 Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 22 Dec 2025 08:37:21 +0000 Subject: [PATCH 07/13] fs(feat): Add Backup Saves for Custom Save Paths Signed-off-by: Collecting --- src/core/file_sys/savedata_factory.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/file_sys/savedata_factory.h b/src/core/file_sys/savedata_factory.h index 33a713359..54250515d 100644 --- a/src/core/file_sys/savedata_factory.h +++ b/src/core/file_sys/savedata_factory.h @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -27,7 +28,7 @@ using ProgramId = u64; class SaveDataFactory { public: explicit SaveDataFactory(Core::System& system_, ProgramId program_id_, - VirtualDir save_directory_); + VirtualDir save_directory_, VirtualDir backup_directory_ = nullptr); ~SaveDataFactory(); VirtualDir Create(SaveDataSpaceId space, const SaveDataAttribute& meta) const; @@ -54,11 +55,13 @@ public: const SaveDataAttribute& attribute) const; void SetAutoCreate(bool state); + void DoNandBackup(SaveDataSpaceId space, const SaveDataAttribute& meta, VirtualDir custom_dir) const; private: Core::System& system; ProgramId program_id; VirtualDir dir; + VirtualDir backup_dir; // This will hold the NAND path bool auto_create{true}; }; From 16f928df79e7cdc65a5ccae392ccfe4b9d337181 Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 22 Dec 2025 08:37:57 +0000 Subject: [PATCH 08/13] fs(feat): Add Backup Saves for Custom Save Paths Signed-off-by: Collecting --- src/core/file_sys/savedata_factory.cpp | 45 ++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/src/core/file_sys/savedata_factory.cpp b/src/core/file_sys/savedata_factory.cpp index b14b52c28..3ca2e0a6d 100644 --- a/src/core/file_sys/savedata_factory.cpp +++ b/src/core/file_sys/savedata_factory.cpp @@ -7,8 +7,10 @@ #include "common/assert.h" #include "common/common_types.h" #include "common/logging/log.h" +#include "common/settings.h" #include "common/uuid.h" #include "core/core.h" +#include "core/file_sys/directory_save_data_filesystem.h" #include "core/file_sys/errors.h" #include "core/file_sys/savedata_extra_data_accessor.h" #include "core/file_sys/savedata_factory.h" @@ -58,8 +60,9 @@ std::string GetFutureSaveDataPath(SaveDataSpaceId space_id, SaveDataType type, u } // Anonymous namespace SaveDataFactory::SaveDataFactory(Core::System& system_, ProgramId program_id_, - VirtualDir save_directory_) - : system{system_}, program_id{program_id_}, dir{std::move(save_directory_)} { + VirtualDir save_directory_, VirtualDir backup_directory_) + : system{system_}, program_id{program_id_}, dir{std::move(save_directory_)}, + backup_dir{std::move(backup_directory_)} { // Delete all temporary storages // On hardware, it is expected that temporary storage be empty at first use. dir->DeleteSubdirectoryRecursive("temp"); @@ -100,7 +103,6 @@ VirtualDir SaveDataFactory::Create(SaveDataSpaceId space, const SaveDataAttribut } VirtualDir SaveDataFactory::Open(SaveDataSpaceId space, const SaveDataAttribute& meta) const { - const auto save_directory = GetFullPath(program_id, dir, space, meta.type, meta.program_id, meta.user_id, meta.system_save_data_id); @@ -324,4 +326,41 @@ Result SaveDataFactory::WriteSaveDataExtraDataWithMask(const SaveDataExtraData& return ResultSuccess; } +void SaveDataFactory::DoNandBackup(SaveDataSpaceId space, const SaveDataAttribute& meta, VirtualDir custom_dir) const { + LOG_INFO(Common, "Dual-Save: Backup process initiated for Program ID: {:016X}", program_id); + + if (!Settings::values.backup_saves_to_nand.GetValue()) { + LOG_INFO(Common, "Dual-Save: Backup skipped (Setting is OFF)"); + return; + } + + if (backup_dir == nullptr) { + LOG_ERROR(Common, "Dual-Save: Backup failed (NAND directory is NULL)"); + return; + } + + if (custom_dir == nullptr) { + LOG_ERROR(Common, "Dual-Save: Backup failed (Source Custom directory is NULL)"); + return; + } + + const auto nand_path = GetFullPath(program_id, backup_dir, space, meta.type, meta.program_id, + meta.user_id, meta.system_save_data_id); + + auto nand_out = backup_dir->CreateDirectoryRelative(nand_path); + if (nand_out != nullptr) { + LOG_INFO(Common, "Dual-Save: Mirroring files to NAND: {}", nand_path); + + // Clear the old backup + nand_out->CleanSubdirectoryRecursive("."); + + // Perform the copy + VfsRawCopyD(custom_dir, nand_out); + + LOG_INFO(Common, "Dual-Save: NAND Backup successful."); + } else { + LOG_ERROR(Common, "Dual-Save: Could not create/access NAND backup path!"); + } +} + } // namespace FileSys From 7ad7d8ada63e366c881ff31b93d9a1b859acd4d9 Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 22 Dec 2025 08:38:42 +0000 Subject: [PATCH 09/13] fs(feat): Add Backup Saves for Custom Save Paths Signed-off-by: Collecting --- src/core/hle/service/filesystem/filesystem.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/core/hle/service/filesystem/filesystem.cpp b/src/core/hle/service/filesystem/filesystem.cpp index 1a3637fda..edb7d6ede 100644 --- a/src/core/hle/service/filesystem/filesystem.cpp +++ b/src/core/hle/service/filesystem/filesystem.cpp @@ -431,7 +431,12 @@ std::shared_ptr FileSystemController::CreateSaveDataFa if (!custom_path_str.empty() && Common::FS::IsDir(custom_path)) { LOG_INFO(Service_FS, "Using custom save path for program_id={:016X}: {}", program_id, custom_path_str); auto custom_save_directory = vfs->OpenDirectory(custom_path_str, rw_mode); - return std::make_shared(system, program_id, std::move(custom_save_directory)); + + // Fetch the default NAND directory to act as the backup location + auto nand_directory = vfs->OpenDirectory(Common::FS::GetCitronPathString(CitronPath::NANDDir), rw_mode); + + return std::make_shared( + system, program_id, std::move(custom_save_directory), std::move(nand_directory)); } } From 42c237dc4a71f8847f2a9f6e46c4f6ef744c8ca3 Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 22 Dec 2025 08:39:20 +0000 Subject: [PATCH 10/13] fs(feat): Add Backup Saves for Custom Save Paths Signed-off-by: Collecting --- src/core/hle/service/filesystem/fsp/fs_i_filesystem.h | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/core/hle/service/filesystem/fsp/fs_i_filesystem.h b/src/core/hle/service/filesystem/fsp/fs_i_filesystem.h index dd069f36f..ee9588581 100644 --- a/src/core/hle/service/filesystem/fsp/fs_i_filesystem.h +++ b/src/core/hle/service/filesystem/fsp/fs_i_filesystem.h @@ -1,10 +1,12 @@ // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "common/common_funcs.h" #include "core/file_sys/fs_filesystem.h" +#include "core/file_sys/fs_save_data_types.h" #include "core/file_sys/fsa/fs_i_filesystem.h" #include "core/file_sys/vfs/vfs.h" #include "core/hle/service/cmif_types.h" @@ -23,7 +25,10 @@ class IDirectory; class IFileSystem final : public ServiceFramework { public: - explicit IFileSystem(Core::System& system_, FileSys::VirtualDir dir_, SizeGetter size_getter_); + explicit IFileSystem(Core::System& system_, FileSys::VirtualDir dir_, SizeGetter size_getter_, + std::shared_ptr factory_ = nullptr, + FileSys::SaveDataSpaceId space_id_ = {}, + FileSys::SaveDataAttribute attribute_ = {}); Result CreateFile(const InLargeData path, s32 option, s64 size); @@ -55,6 +60,10 @@ public: private: std::unique_ptr backend; SizeGetter size_getter; + FileSys::VirtualDir content_dir; + std::shared_ptr save_factory; + FileSys::SaveDataSpaceId save_space; + FileSys::SaveDataAttribute save_attr; }; } // namespace Service::FileSystem From 625e1d32292cc29bb02794df232bdf3ed6c0e492 Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 22 Dec 2025 08:39:59 +0000 Subject: [PATCH 11/13] fs(feat): Add Backup Saves for Custom Save Paths Signed-off-by: Collecting --- .../filesystem/fsp/fs_i_filesystem.cpp | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/core/hle/service/filesystem/fsp/fs_i_filesystem.cpp b/src/core/hle/service/filesystem/fsp/fs_i_filesystem.cpp index b4cd22d60..c21285a96 100644 --- a/src/core/hle/service/filesystem/fsp/fs_i_filesystem.cpp +++ b/src/core/hle/service/filesystem/fsp/fs_i_filesystem.cpp @@ -1,7 +1,9 @@ // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "common/string_util.h" +#include "common/settings.h" #include "core/file_sys/fssrv/fssrv_sf_path.h" #include "core/hle/service/cmif_serialization.h" #include "core/hle/service/filesystem/fsp/fs_i_directory.h" @@ -10,10 +12,16 @@ namespace Service::FileSystem { -IFileSystem::IFileSystem(Core::System& system_, FileSys::VirtualDir dir_, SizeGetter size_getter_) - : ServiceFramework{system_, "IFileSystem"}, backend{std::make_unique( - dir_)}, - size_getter{std::move(size_getter_)} { +IFileSystem::IFileSystem(Core::System& system_, FileSys::VirtualDir dir_, SizeGetter size_getter_, + std::shared_ptr factory_, + FileSys::SaveDataSpaceId space_id_, FileSys::SaveDataAttribute attribute_) + : ServiceFramework{system_, "IFileSystem"}, + backend{std::make_unique(dir_)}, + size_getter{std::move(size_getter_)}, + content_dir{std::move(dir_)}, + save_factory{std::move(factory_)}, + save_space{space_id_}, + save_attr{attribute_} { static const FunctionInfo functions[] = { {0, D<&IFileSystem::CreateFile>, "CreateFile"}, {1, D<&IFileSystem::DeleteFile>, "DeleteFile"}, @@ -124,11 +132,15 @@ Result IFileSystem::GetEntryType( } Result IFileSystem::Commit() { - LOG_DEBUG(Service_FS, "called"); + Result res = backend->Commit(); + if (res != ResultSuccess) return res; - // Based on LibHac DirectorySaveDataFileSystem::DoCommit - // The backend FSA layer should handle the actual commit logic - R_RETURN(backend->Commit()); + if (save_factory && Settings::values.backup_saves_to_nand.GetValue()) { + LOG_INFO(Common, "IFileSystem: Commit detected, triggering NAND backup..."); + save_factory->DoNandBackup(save_space, save_attr, content_dir); + } + + R_RETURN(res); } Result IFileSystem::GetFreeSpaceSize( From 9407a5ba94f718aefc2a959a9b93f7a759449e4e Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 22 Dec 2025 08:40:29 +0000 Subject: [PATCH 12/13] fs(feat): Add Backup Saves for Custom Save Paths Signed-off-by: Collecting --- src/core/hle/service/filesystem/fsp/fsp_srv.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/hle/service/filesystem/fsp/fsp_srv.cpp b/src/core/hle/service/filesystem/fsp/fsp_srv.cpp index 70cd31926..32c79af63 100644 --- a/src/core/hle/service/filesystem/fsp/fsp_srv.cpp +++ b/src/core/hle/service/filesystem/fsp/fsp_srv.cpp @@ -280,8 +280,9 @@ Result FSP_SRV::OpenSaveDataFileSystem(OutInterface out_interface, break; } - *out_interface = - std::make_shared(system, std::move(dir), SizeGetter::FromStorageId(fsc, id)); + *out_interface = std::make_shared( + system, std::move(dir), SizeGetter::FromStorageId(fsc, id), + save_data_controller->GetFactory(), space_id, attribute); R_SUCCEED(); } From 3dee7e3015846eb798014764537f4a8af55ff14b Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 22 Dec 2025 08:40:51 +0000 Subject: [PATCH 13/13] fs(feat): Add Backup Saves for Custom Save Paths Signed-off-by: Collecting --- src/core/hle/service/filesystem/save_data_controller.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/hle/service/filesystem/save_data_controller.h b/src/core/hle/service/filesystem/save_data_controller.h index 2117f9adb..61be15465 100644 --- a/src/core/hle/service/filesystem/save_data_controller.h +++ b/src/core/hle/service/filesystem/save_data_controller.h @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -38,6 +39,10 @@ public: FileSys::SaveDataSpaceId space, const FileSys::SaveDataAttribute& attribute); + std::shared_ptr GetFactory() const { + return factory; + } + void SetAutoCreate(bool state); private: