mirror of
https://git.eden-emu.dev/archive/citron
synced 2026-04-13 16:20:46 -04:00
feat(fs): Cross-Compatible Emulator Save Pathing w/ Custom Save Paths
Signed-off-by: Collecting <collecting@noreply.localhost>
This commit is contained in:
@@ -3,7 +3,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
@@ -15,56 +17,70 @@
|
|||||||
#include "core/file_sys/savedata_extra_data_accessor.h"
|
#include "core/file_sys/savedata_extra_data_accessor.h"
|
||||||
#include "core/file_sys/savedata_factory.h"
|
#include "core/file_sys/savedata_factory.h"
|
||||||
#include "core/file_sys/vfs/vfs.h"
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
#include "core/file_sys/vfs/vfs_real.h"
|
||||||
|
#include "core/hle/service/acc/profile_manager.h"
|
||||||
|
|
||||||
namespace FileSys {
|
namespace FileSys {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
// Using a leaked raw pointer for the RealVfsFilesystem singleton.
|
||||||
|
// This prevents SIGSEGV during shutdown by ensuring the VFS bridge
|
||||||
|
// outlives all threads that might still be flushing save data.
|
||||||
|
RealVfsFilesystem* GetPersistentVfs() {
|
||||||
|
static RealVfsFilesystem* instance = new RealVfsFilesystem();
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
bool ShouldSaveDataBeAutomaticallyCreated(SaveDataSpaceId space, const SaveDataAttribute& attr) {
|
bool ShouldSaveDataBeAutomaticallyCreated(SaveDataSpaceId space, const SaveDataAttribute& attr) {
|
||||||
return attr.type == SaveDataType::Cache || attr.type == SaveDataType::Temporary ||
|
return attr.type == SaveDataType::Cache || attr.type == SaveDataType::Temporary ||
|
||||||
(space == SaveDataSpaceId::User && ///< Normal Save Data -- Current Title & User
|
(space == SaveDataSpaceId::User &&
|
||||||
(attr.type == SaveDataType::Account || attr.type == SaveDataType::Device) &&
|
(attr.type == SaveDataType::Account || attr.type == SaveDataType::Device) &&
|
||||||
attr.program_id == 0 && attr.system_save_data_id == 0);
|
attr.program_id == 0 && attr.system_save_data_id == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string GetFutureSaveDataPath(SaveDataSpaceId space_id, SaveDataType type, u64 title_id,
|
std::string GetFutureSaveDataPath(SaveDataSpaceId space_id, SaveDataType type, u64 title_id,
|
||||||
u128 user_id) {
|
u128 user_id) {
|
||||||
// Only detect nand user saves.
|
if (space_id != SaveDataSpaceId::User) {
|
||||||
const auto space_id_path = [space_id]() -> std::string_view {
|
|
||||||
switch (space_id) {
|
|
||||||
case SaveDataSpaceId::User:
|
|
||||||
return "/user/save";
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}();
|
|
||||||
|
|
||||||
if (space_id_path.empty()) {
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
Common::UUID uuid;
|
Common::UUID uuid;
|
||||||
std::memcpy(uuid.uuid.data(), user_id.data(), sizeof(Common::UUID));
|
std::memcpy(uuid.uuid.data(), user_id.data(), sizeof(Common::UUID));
|
||||||
|
|
||||||
// Only detect account/device saves from the future location.
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case SaveDataType::Account:
|
case SaveDataType::Account:
|
||||||
return fmt::format("{}/account/{}/{:016X}/0", space_id_path, uuid.RawString(), title_id);
|
return fmt::format("/user/save/account/{}/{:016X}/0", uuid.RawString(), title_id);
|
||||||
case SaveDataType::Device:
|
case SaveDataType::Device:
|
||||||
return fmt::format("{}/device/{:016X}/0", space_id_path, title_id);
|
return fmt::format("/user/save/device/{:016X}/0", title_id);
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BufferedVfsCopy(VirtualFile source, VirtualFile dest) {
|
||||||
|
if (!source || !dest) return;
|
||||||
|
try {
|
||||||
|
std::vector<u8> buffer(0x100000); // 1MB buffer
|
||||||
|
dest->Resize(0);
|
||||||
|
size_t offset = 0;
|
||||||
|
while (offset < source->GetSize()) {
|
||||||
|
const size_t to_read = std::min(buffer.size(), source->GetSize() - offset);
|
||||||
|
source->Read(buffer.data(), to_read, offset);
|
||||||
|
dest->Write(buffer.data(), to_read, offset);
|
||||||
|
offset += to_read;
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
LOG_ERROR(Service_FS, "Critical error during VFS mirror operation.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // Anonymous namespace
|
} // Anonymous namespace
|
||||||
|
|
||||||
SaveDataFactory::SaveDataFactory(Core::System& system_, ProgramId program_id_,
|
SaveDataFactory::SaveDataFactory(Core::System& system_, ProgramId program_id_,
|
||||||
VirtualDir save_directory_, VirtualDir backup_directory_)
|
VirtualDir save_directory_, VirtualDir backup_directory_)
|
||||||
: system{system_}, program_id{program_id_}, dir{std::move(save_directory_)},
|
: system{system_}, program_id{program_id_}, dir{std::move(save_directory_)},
|
||||||
backup_dir{std::move(backup_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");
|
dir->DeleteSubdirectoryRecursive("temp");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,19 +95,14 @@ VirtualDir SaveDataFactory::Create(SaveDataSpaceId space, const SaveDataAttribut
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize ExtraData for new save
|
|
||||||
SaveDataExtraDataAccessor accessor(save_dir);
|
SaveDataExtraDataAccessor accessor(save_dir);
|
||||||
if (accessor.Initialize(true) != ResultSuccess) {
|
if (accessor.Initialize(true) == ResultSuccess) {
|
||||||
LOG_WARNING(Service_FS, "Failed to initialize ExtraData for new save at {}", save_directory);
|
|
||||||
// Continue anyway - save is still usable
|
|
||||||
} else {
|
|
||||||
// Write initial extra data
|
|
||||||
SaveDataExtraData initial_data{};
|
SaveDataExtraData initial_data{};
|
||||||
initial_data.attr = meta;
|
initial_data.attr = meta;
|
||||||
initial_data.owner_id = meta.program_id;
|
initial_data.owner_id = meta.program_id;
|
||||||
initial_data.timestamp = std::chrono::system_clock::now().time_since_epoch().count();
|
initial_data.timestamp = std::chrono::system_clock::now().time_since_epoch().count();
|
||||||
initial_data.flags = static_cast<u32>(SaveDataFlags::None);
|
initial_data.flags = static_cast<u32>(SaveDataFlags::None);
|
||||||
initial_data.available_size = 0; // Will be updated on commit
|
initial_data.available_size = 0;
|
||||||
initial_data.journal_size = 0;
|
initial_data.journal_size = 0;
|
||||||
initial_data.commit_id = 1;
|
initial_data.commit_id = 1;
|
||||||
|
|
||||||
@@ -122,6 +133,8 @@ VirtualDir SaveDataFactory::GetSaveDataSpaceDirectory(SaveDataSpaceId space) con
|
|||||||
std::string SaveDataFactory::GetSaveDataSpaceIdPath(SaveDataSpaceId space) {
|
std::string SaveDataFactory::GetSaveDataSpaceIdPath(SaveDataSpaceId space) {
|
||||||
switch (space) {
|
switch (space) {
|
||||||
case SaveDataSpaceId::System:
|
case SaveDataSpaceId::System:
|
||||||
|
case SaveDataSpaceId::ProperSystem:
|
||||||
|
case SaveDataSpaceId::SafeMode:
|
||||||
return "/system/";
|
return "/system/";
|
||||||
case SaveDataSpaceId::User:
|
case SaveDataSpaceId::User:
|
||||||
return "/user/";
|
return "/user/";
|
||||||
@@ -130,54 +143,37 @@ std::string SaveDataFactory::GetSaveDataSpaceIdPath(SaveDataSpaceId space) {
|
|||||||
case SaveDataSpaceId::SdSystem:
|
case SaveDataSpaceId::SdSystem:
|
||||||
case SaveDataSpaceId::SdUser:
|
case SaveDataSpaceId::SdUser:
|
||||||
return "/sd/";
|
return "/sd/";
|
||||||
case SaveDataSpaceId::ProperSystem:
|
|
||||||
return "/system/";
|
|
||||||
case SaveDataSpaceId::SafeMode:
|
|
||||||
return "/system/";
|
|
||||||
default:
|
default:
|
||||||
ASSERT_MSG(false, "Unrecognized SaveDataSpaceId: {:02X}", static_cast<u8>(space));
|
return "/unrecognized/";
|
||||||
return "/unrecognized/"; ///< To prevent corruption when ignoring asserts.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string SaveDataFactory::GetFullPath(ProgramId program_id, VirtualDir dir,
|
std::string SaveDataFactory::GetFullPath(ProgramId program_id, VirtualDir dir,
|
||||||
SaveDataSpaceId space, SaveDataType type, u64 title_id,
|
SaveDataSpaceId space, SaveDataType type, u64 title_id,
|
||||||
u128 user_id, u64 save_id) {
|
u128 user_id, u64 save_id) {
|
||||||
// According to switchbrew, if a save is of type SaveData and the title id field is 0, it should
|
if ((type == SaveDataType::Account || type == SaveDataType::Device) && title_id == 0) {
|
||||||
// be interpreted as the title id of the current process.
|
title_id = program_id;
|
||||||
if (type == SaveDataType::Account || type == SaveDataType::Device) {
|
|
||||||
if (title_id == 0) {
|
|
||||||
title_id = program_id;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For compat with a future impl.
|
if (std::string future_path = GetFutureSaveDataPath(space, type, title_id & ~(0xFFULL), user_id);
|
||||||
if (std::string future_path =
|
|
||||||
GetFutureSaveDataPath(space, type, title_id & ~(0xFFULL), user_id);
|
|
||||||
!future_path.empty()) {
|
!future_path.empty()) {
|
||||||
// Check if this location exists, and prefer it over the old.
|
if (dir->GetDirectoryRelative(future_path) != nullptr) {
|
||||||
if (const auto future_dir = dir->GetDirectoryRelative(future_path); future_dir != nullptr) {
|
|
||||||
LOG_INFO(Service_FS, "Using save at new location: {}", future_path);
|
|
||||||
return future_path;
|
return future_path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string out = GetSaveDataSpaceIdPath(space);
|
std::string out = GetSaveDataSpaceIdPath(space);
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case SaveDataType::System:
|
case SaveDataType::System:
|
||||||
return fmt::format("{}save/{:016X}/{:016X}{:016X}", out, save_id, user_id[1], user_id[0]);
|
return fmt::format("{}save/{:016X}/{:016X}{:016X}", out, save_id, user_id[1], user_id[0]);
|
||||||
case SaveDataType::Account:
|
case SaveDataType::Account:
|
||||||
case SaveDataType::Device:
|
case SaveDataType::Device:
|
||||||
return fmt::format("{}save/{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0],
|
return fmt::format("{}save/{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0], title_id);
|
||||||
title_id);
|
|
||||||
case SaveDataType::Temporary:
|
case SaveDataType::Temporary:
|
||||||
return fmt::format("{}{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0],
|
return fmt::format("{}{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0], title_id);
|
||||||
title_id);
|
|
||||||
case SaveDataType::Cache:
|
case SaveDataType::Cache:
|
||||||
return fmt::format("{}save/cache/{:016X}", out, title_id);
|
return fmt::format("{}save/cache/{:016X}", out, title_id);
|
||||||
default:
|
default:
|
||||||
ASSERT_MSG(false, "Unrecognized SaveDataType: {:02X}", static_cast<u8>(type));
|
|
||||||
return fmt::format("{}save/unknown_{:X}/{:016X}", out, static_cast<u8>(type), title_id);
|
return fmt::format("{}save/unknown_{:X}/{:016X}", out, static_cast<u8>(type), title_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,36 +187,21 @@ std::string SaveDataFactory::GetUserGameSaveDataRoot(u128 user_id, bool future)
|
|||||||
return fmt::format("/user/save/{:016X}/{:016X}{:016X}", 0, user_id[1], user_id[0]);
|
return fmt::format("/user/save/{:016X}/{:016X}{:016X}", 0, user_id[1], user_id[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
SaveDataSize SaveDataFactory::ReadSaveDataSize(SaveDataType type, u64 title_id,
|
SaveDataSize SaveDataFactory::ReadSaveDataSize(SaveDataType type, u64 title_id, u128 user_id) const {
|
||||||
u128 user_id) const {
|
const auto path = GetFullPath(program_id, dir, SaveDataSpaceId::User, type, title_id, user_id, 0);
|
||||||
const auto path =
|
|
||||||
GetFullPath(program_id, dir, SaveDataSpaceId::User, type, title_id, user_id, 0);
|
|
||||||
const auto relative_dir = GetOrCreateDirectoryRelative(dir, path);
|
const auto relative_dir = GetOrCreateDirectoryRelative(dir, path);
|
||||||
|
|
||||||
const auto size_file = relative_dir->GetFile(GetSaveDataSizeFileName());
|
const auto size_file = relative_dir->GetFile(GetSaveDataSizeFileName());
|
||||||
if (size_file == nullptr || size_file->GetSize() < sizeof(SaveDataSize)) {
|
if (size_file == nullptr || size_file->GetSize() < sizeof(SaveDataSize)) return {0, 0};
|
||||||
return {0, 0};
|
|
||||||
}
|
|
||||||
|
|
||||||
SaveDataSize out;
|
SaveDataSize out;
|
||||||
if (size_file->ReadObject(&out) != sizeof(SaveDataSize)) {
|
if (size_file->ReadObject(&out) != sizeof(SaveDataSize)) return {0, 0};
|
||||||
return {0, 0};
|
|
||||||
}
|
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SaveDataFactory::WriteSaveDataSize(SaveDataType type, u64 title_id, u128 user_id,
|
void SaveDataFactory::WriteSaveDataSize(SaveDataType type, u64 title_id, u128 user_id, SaveDataSize new_value) const {
|
||||||
SaveDataSize new_value) const {
|
const auto path = GetFullPath(program_id, dir, SaveDataSpaceId::User, type, title_id, user_id, 0);
|
||||||
const auto path =
|
|
||||||
GetFullPath(program_id, dir, SaveDataSpaceId::User, type, title_id, user_id, 0);
|
|
||||||
const auto relative_dir = GetOrCreateDirectoryRelative(dir, path);
|
const auto relative_dir = GetOrCreateDirectoryRelative(dir, path);
|
||||||
|
|
||||||
const auto size_file = relative_dir->CreateFile(GetSaveDataSizeFileName());
|
const auto size_file = relative_dir->CreateFile(GetSaveDataSizeFileName());
|
||||||
if (size_file == nullptr) {
|
if (size_file == nullptr) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_file->Resize(sizeof(SaveDataSize));
|
size_file->Resize(sizeof(SaveDataSize));
|
||||||
size_file->WriteObject(new_value);
|
size_file->WriteObject(new_value);
|
||||||
}
|
}
|
||||||
@@ -229,137 +210,173 @@ void SaveDataFactory::SetAutoCreate(bool state) {
|
|||||||
auto_create = state;
|
auto_create = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
Result SaveDataFactory::ReadSaveDataExtraData(SaveDataExtraData* out_extra_data,
|
Result SaveDataFactory::ReadSaveDataExtraData(SaveDataExtraData* out_extra_data, SaveDataSpaceId space, const SaveDataAttribute& attribute) const {
|
||||||
SaveDataSpaceId space,
|
const auto save_directory = GetFullPath(program_id, dir, space, attribute.type, attribute.program_id, attribute.user_id, attribute.system_save_data_id);
|
||||||
const SaveDataAttribute& attribute) const {
|
|
||||||
const auto save_directory =
|
|
||||||
GetFullPath(program_id, dir, space, attribute.type, attribute.program_id, attribute.user_id,
|
|
||||||
attribute.system_save_data_id);
|
|
||||||
|
|
||||||
auto save_dir = dir->GetDirectoryRelative(save_directory);
|
auto save_dir = dir->GetDirectoryRelative(save_directory);
|
||||||
if (save_dir == nullptr) {
|
if (save_dir == nullptr) return ResultPathNotFound;
|
||||||
return ResultPathNotFound;
|
|
||||||
}
|
|
||||||
|
|
||||||
SaveDataExtraDataAccessor accessor(save_dir);
|
SaveDataExtraDataAccessor accessor(save_dir);
|
||||||
|
if (accessor.Initialize(false) != ResultSuccess) {
|
||||||
// Try to initialize (but don't create if missing)
|
*out_extra_data = {};
|
||||||
if (Result result = accessor.Initialize(false); result != ResultSuccess) {
|
|
||||||
// ExtraData doesn't exist - return default values
|
|
||||||
LOG_DEBUG(Service_FS, "ExtraData not found for save at {}, returning defaults",
|
|
||||||
save_directory);
|
|
||||||
|
|
||||||
// Return zeroed data
|
|
||||||
*out_extra_data = {}; // Or: *out_extra_data = SaveDataExtraData{};
|
|
||||||
out_extra_data->attr = attribute;
|
out_extra_data->attr = attribute;
|
||||||
return ResultSuccess;
|
return ResultSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
return accessor.ReadExtraData(out_extra_data);
|
return accessor.ReadExtraData(out_extra_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
Result SaveDataFactory::WriteSaveDataExtraData(const SaveDataExtraData& extra_data,
|
Result SaveDataFactory::WriteSaveDataExtraData(const SaveDataExtraData& extra_data, SaveDataSpaceId space, const SaveDataAttribute& attribute) const {
|
||||||
SaveDataSpaceId space,
|
const auto save_directory = GetFullPath(program_id, dir, space, attribute.type, attribute.program_id, attribute.user_id, attribute.system_save_data_id);
|
||||||
const SaveDataAttribute& attribute) const {
|
|
||||||
const auto save_directory =
|
|
||||||
GetFullPath(program_id, dir, space, attribute.type, attribute.program_id, attribute.user_id,
|
|
||||||
attribute.system_save_data_id);
|
|
||||||
|
|
||||||
auto save_dir = dir->GetDirectoryRelative(save_directory);
|
auto save_dir = dir->GetDirectoryRelative(save_directory);
|
||||||
if (save_dir == nullptr) {
|
if (save_dir == nullptr) return ResultPathNotFound;
|
||||||
return ResultPathNotFound;
|
|
||||||
}
|
|
||||||
|
|
||||||
SaveDataExtraDataAccessor accessor(save_dir);
|
SaveDataExtraDataAccessor accessor(save_dir);
|
||||||
|
|
||||||
// Initialize and create if missing
|
|
||||||
R_TRY(accessor.Initialize(true));
|
R_TRY(accessor.Initialize(true));
|
||||||
|
|
||||||
// Write the data
|
|
||||||
R_TRY(accessor.WriteExtraData(extra_data));
|
R_TRY(accessor.WriteExtraData(extra_data));
|
||||||
|
return accessor.CommitExtraData();
|
||||||
// Commit immediately for transactional writes
|
|
||||||
R_TRY(accessor.CommitExtraData());
|
|
||||||
|
|
||||||
return ResultSuccess;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Result SaveDataFactory::WriteSaveDataExtraDataWithMask(const SaveDataExtraData& extra_data,
|
Result SaveDataFactory::WriteSaveDataExtraDataWithMask(const SaveDataExtraData& extra_data, const SaveDataExtraData& mask, SaveDataSpaceId space, const SaveDataAttribute& attribute) const {
|
||||||
const SaveDataExtraData& mask,
|
const auto save_directory = GetFullPath(program_id, dir, space, attribute.type, attribute.program_id, attribute.user_id, attribute.system_save_data_id);
|
||||||
SaveDataSpaceId space,
|
|
||||||
const SaveDataAttribute& attribute) const {
|
|
||||||
const auto save_directory =
|
|
||||||
GetFullPath(program_id, dir, space, attribute.type, attribute.program_id, attribute.user_id,
|
|
||||||
attribute.system_save_data_id);
|
|
||||||
|
|
||||||
auto save_dir = dir->GetDirectoryRelative(save_directory);
|
auto save_dir = dir->GetDirectoryRelative(save_directory);
|
||||||
if (save_dir == nullptr) {
|
if (save_dir == nullptr) return ResultPathNotFound;
|
||||||
return ResultPathNotFound;
|
|
||||||
}
|
|
||||||
|
|
||||||
SaveDataExtraDataAccessor accessor(save_dir);
|
SaveDataExtraDataAccessor accessor(save_dir);
|
||||||
|
|
||||||
// Initialize and create if missing
|
|
||||||
R_TRY(accessor.Initialize(true));
|
R_TRY(accessor.Initialize(true));
|
||||||
|
|
||||||
// Read existing data
|
|
||||||
SaveDataExtraData current_data{};
|
SaveDataExtraData current_data{};
|
||||||
R_TRY(accessor.ReadExtraData(¤t_data));
|
R_TRY(accessor.ReadExtraData(¤t_data));
|
||||||
|
|
||||||
// Apply mask: copy only the bytes where mask is non-zero
|
|
||||||
const u8* extra_data_bytes = reinterpret_cast<const u8*>(&extra_data);
|
const u8* extra_data_bytes = reinterpret_cast<const u8*>(&extra_data);
|
||||||
const u8* mask_bytes = reinterpret_cast<const u8*>(&mask);
|
const u8* mask_bytes = reinterpret_cast<const u8*>(&mask);
|
||||||
u8* current_data_bytes = reinterpret_cast<u8*>(¤t_data);
|
u8* current_data_bytes = reinterpret_cast<u8*>(¤t_data);
|
||||||
|
|
||||||
for (size_t i = 0; i < sizeof(SaveDataExtraData); ++i) {
|
for (size_t i = 0; i < sizeof(SaveDataExtraData); ++i) {
|
||||||
if (mask_bytes[i] != 0) {
|
if (mask_bytes[i] != 0) current_data_bytes[i] = extra_data_bytes[i];
|
||||||
current_data_bytes[i] = extra_data_bytes[i];
|
}
|
||||||
|
R_TRY(accessor.WriteExtraData(current_data));
|
||||||
|
return accessor.CommitExtraData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- MIRRORING TOOLS ---
|
||||||
|
|
||||||
|
VirtualDir SaveDataFactory::GetMirrorDirectory(u64 title_id) const {
|
||||||
|
auto it = Settings::values.mirrored_save_paths.find(title_id);
|
||||||
|
if (it == Settings::values.mirrored_save_paths.end() || it->second.empty()) return nullptr;
|
||||||
|
|
||||||
|
std::filesystem::path host_path(it->second);
|
||||||
|
if (!std::filesystem::exists(host_path)) return nullptr;
|
||||||
|
|
||||||
|
// Get the persistent VFS bridge
|
||||||
|
auto* vfs = GetPersistentVfs();
|
||||||
|
return vfs->OpenDirectory(it->second, OpenMode::ReadWrite);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SaveDataFactory::SmartSyncFromSource(VirtualDir source, VirtualDir dest) const {
|
||||||
|
// Citron: Shutdown and null safety
|
||||||
|
if (!source || !dest || system.IsShuttingDown()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync files from Source to Destination
|
||||||
|
for (const auto& s_file : source->GetFiles()) {
|
||||||
|
if (!s_file) continue;
|
||||||
|
std::string name = s_file->GetName();
|
||||||
|
|
||||||
|
// Skip metadata and lock files
|
||||||
|
if (name == ".lock" || name == ".citron_save_size" || name.find("mirror_backup") != std::string::npos) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto d_file = dest->CreateFile(name);
|
||||||
|
if (d_file) {
|
||||||
|
BufferedVfsCopy(s_file, d_file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write back the masked data
|
// Recurse into subdirectories
|
||||||
R_TRY(accessor.WriteExtraData(current_data));
|
for (const auto& s_subdir : source->GetSubdirectories()) {
|
||||||
|
if (!s_subdir) continue;
|
||||||
|
|
||||||
// Commit the changes
|
// Prevent recursion into title-id-named folders to avoid infinite loops
|
||||||
R_TRY(accessor.CommitExtraData());
|
if (s_subdir->GetName().find("0100") != std::string::npos) continue;
|
||||||
|
|
||||||
return ResultSuccess;
|
auto d_subdir = dest->GetDirectoryRelative(s_subdir->GetName());
|
||||||
|
if (!d_subdir) {
|
||||||
|
d_subdir = dest->CreateDirectoryRelative(s_subdir->GetName());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d_subdir) {
|
||||||
|
SmartSyncFromSource(s_subdir, d_subdir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SaveDataFactory::PerformStartupMirrorSync() const {
|
||||||
|
// If settings are empty or system is shutting down/uninitialized
|
||||||
|
if (Settings::values.mirrored_save_paths.empty() || system.IsShuttingDown()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure our NAND directory is actually valid
|
||||||
|
if (!dir) {
|
||||||
|
LOG_ERROR(Service_FS, "Mirroring: Startup Sync aborted. NAND directory is null.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to locate the save root with null checks at every step
|
||||||
|
VirtualDir user_save_root = nullptr;
|
||||||
|
try {
|
||||||
|
user_save_root = dir->GetDirectoryRelative("user/save/0000000000000000");
|
||||||
|
if (!user_save_root) {
|
||||||
|
user_save_root = dir->GetDirectoryRelative("user/save");
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
LOG_ERROR(Service_FS, "Mirroring: Critical failure accessing VFS. Filesystem may be stale.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user_save_root) {
|
||||||
|
LOG_WARNING(Service_FS, "Mirroring: Could not find user save root in NAND.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO(Service_FS, "Mirroring: Startup Sync initiated.");
|
||||||
|
|
||||||
|
for (const auto& [title_id, host_path] : Settings::values.mirrored_save_paths) {
|
||||||
|
if (host_path.empty()) continue;
|
||||||
|
|
||||||
|
auto mirror_source = GetMirrorDirectory(title_id);
|
||||||
|
if (!mirror_source) continue;
|
||||||
|
|
||||||
|
std::string title_id_str = fmt::format("{:016X}", title_id);
|
||||||
|
|
||||||
|
for (const auto& profile_dir : user_save_root->GetSubdirectories()) {
|
||||||
|
if (!profile_dir) continue;
|
||||||
|
|
||||||
|
auto nand_dest = profile_dir->GetDirectoryRelative(title_id_str);
|
||||||
|
|
||||||
|
if (!nand_dest) {
|
||||||
|
for (const auto& sub : profile_dir->GetSubdirectories()) {
|
||||||
|
if (!sub) continue;
|
||||||
|
nand_dest = sub->GetDirectoryRelative(title_id_str);
|
||||||
|
if (nand_dest) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nand_dest) {
|
||||||
|
LOG_INFO(Service_FS, "Mirroring: Pulling external data for {}", title_id_str);
|
||||||
|
SmartSyncFromSource(mirror_source, nand_dest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SaveDataFactory::DoNandBackup(SaveDataSpaceId space, const SaveDataAttribute& meta, VirtualDir custom_dir) const {
|
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);
|
u64 title_id = (meta.program_id != 0 ? meta.program_id : static_cast<u64>(program_id));
|
||||||
|
if (Settings::values.mirrored_save_paths.count(title_id)) return;
|
||||||
|
|
||||||
if (!Settings::values.backup_saves_to_nand.GetValue()) {
|
if (!Settings::values.backup_saves_to_nand.GetValue() || backup_dir == nullptr || custom_dir == nullptr) return;
|
||||||
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);
|
|
||||||
|
|
||||||
|
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);
|
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
|
if (nand_out) {
|
||||||
nand_out->CleanSubdirectoryRecursive(".");
|
nand_out->CleanSubdirectoryRecursive(".");
|
||||||
|
|
||||||
// Perform the copy
|
|
||||||
VfsRawCopyD(custom_dir, nand_out);
|
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!");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user