From a78dd3456ae03f07f8979b9fe36782c030d83c98 Mon Sep 17 00:00:00 2001 From: J-D-K Date: Wed, 24 Sep 2025 01:57:26 -0400 Subject: [PATCH 1/4] Begin generic SaveImportState --- include/appstates/SaveImportState.hpp | 53 ++++++++++++++++++++++ include/appstates/UserOptionState.hpp | 3 ++ romfs/Text/ENUS.json | 5 ++- source/appstates/SaveImportState.cpp | 64 +++++++++++++++++++++++++++ source/appstates/UserOptionState.cpp | 22 ++++++--- source/logging/logger.cpp | 2 +- 6 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 include/appstates/SaveImportState.hpp create mode 100644 source/appstates/SaveImportState.cpp diff --git a/include/appstates/SaveImportState.hpp b/include/appstates/SaveImportState.hpp new file mode 100644 index 0000000..5957691 --- /dev/null +++ b/include/appstates/SaveImportState.hpp @@ -0,0 +1,53 @@ +#pragma once +#include "StateManager.hpp" +#include "appstates/BaseState.hpp" +#include "data/data.hpp" +#include "fslib.hpp" +#include "ui/ui.hpp" + +#include + +class SaveImportState final : public BaseState +{ + public: + /// @brief Constructs a new SaveImportState. + SaveImportState(data::User *user); + + static inline std::shared_ptr create(data::User *user) + { + return std::make_shared(user); + } + + static inline std::shared_ptr create_and_push(data::User *user) + { + auto newState = SaveImportState::create(user); + StateManager::push_state(newState); + return newState; + } + + void update() override; + + void render() override; + + private: + /// @brief Pointer to the target user. + data::User *m_user{}; + + /// @brief + fslib::Directory m_saveDir{}; + + /// @brief Menu shared by all instances. + static inline std::shared_ptr sm_saveMenu{}; + + /// @brief Panel shared by all instances. + static inline std::shared_ptr sm_slidePanel{}; + + /// @brief Ensures the menu and panel are initialized correctly. + void initialize_static_members(); + + /// @brief Opens and + void initialize_directory_menu(); + + /// @brief Cleans up and deactivates the state. + void deactivate_state(); +}; diff --git a/include/appstates/UserOptionState.hpp b/include/appstates/UserOptionState.hpp index 0cd1afa..22b004a 100644 --- a/include/appstates/UserOptionState.hpp +++ b/include/appstates/UserOptionState.hpp @@ -91,6 +91,9 @@ class UserOptionState final : public BaseState /// @brief Deletes all save data for the user. void delete_all_save_data(); + /// @brief Creates and pushes the save import state. + void create_push_save_import(); + /// @brief Performs operations to reset static members and marks the state for purging. void deactivate_state(); }; diff --git a/romfs/Text/ENUS.json b/romfs/Text/ENUS.json index cc4a563..6cae80d 100644 --- a/romfs/Text/ENUS.json +++ b/romfs/Text/ENUS.json @@ -278,7 +278,8 @@ "0: Dump all for `%s`", "1: Create Save Data for `%s`", "2: Create All Save Data for `%s`", - "3: Delete All Save Data for `%s`" + "3: Delete All Save Data for `%s`", + "4: Import Save Data for `%s`" ], "WebDavStrings": [ "0: WebDav successfully started!", @@ -289,4 +290,4 @@ "1: No [B]", "2: OK [A]" ] -} \ No newline at end of file +} diff --git a/source/appstates/SaveImportState.cpp b/source/appstates/SaveImportState.cpp new file mode 100644 index 0000000..dce9708 --- /dev/null +++ b/source/appstates/SaveImportState.cpp @@ -0,0 +1,64 @@ +#include "appstates/SaveImportState.hpp" + +#include "config/config.hpp" +#include "input.hpp" + +SaveImportState::SaveImportState(data::User *user) + : m_user(user) + , m_saveDir() +{ + SaveImportState::initialize_static_members(); + SaveImportState::initialize_directory_menu(); +} + +void SaveImportState::update() +{ + const bool hasFocus = BaseState::has_focus(); + + sm_slidePanel->update(hasFocus); + if (!sm_slidePanel->is_open()) { return; } + + const bool aPressed = input::button_pressed(HidNpadButton_A); + const bool bPressed = input::button_pressed(HidNpadButton_B); + + if (aPressed) {} + else if (bPressed) { sm_slidePanel->close(); } + else if (sm_slidePanel->is_closed()) { SaveImportState::deactivate_state(); } +} + +void SaveImportState::render() +{ + const bool hasFocus = BaseState::has_focus(); + + sm_slidePanel->clear_target(); + sm_slidePanel->render(sdl::Texture::Null, hasFocus); +} + +void SaveImportState::initialize_static_members() +{ + static constexpr int SIZE_PANEL_WIDTH = 512; + + if (sm_saveMenu && sm_slidePanel) { return; } + + sm_saveMenu = ui::Menu::create(8, 8, 492, 22, 720); + sm_slidePanel = ui::SlideOutPanel::create(SIZE_PANEL_WIDTH, ui::SlideOutPanel::Side::Right); + + sm_slidePanel->push_new_element(sm_saveMenu); +} + +void SaveImportState::initialize_directory_menu() +{ + const fslib::Path saveDirPath{config::get_working_directory() / "saves"}; + + m_saveDir.open(saveDirPath); + if (!m_saveDir.is_open()) { return; } + + for (const fslib::DirectoryEntry &entry : m_saveDir) { sm_saveMenu->add_option(entry.get_filename()); } +} + +void SaveImportState::deactivate_state() +{ + sm_saveMenu->reset(); + sm_slidePanel->reset(); + BaseState::deactivate(); +} diff --git a/source/appstates/UserOptionState.cpp b/source/appstates/UserOptionState.cpp index ed2c536..58745cc 100644 --- a/source/appstates/UserOptionState.cpp +++ b/source/appstates/UserOptionState.cpp @@ -5,6 +5,7 @@ #include "appstates/MainMenuState.hpp" #include "appstates/ProgressState.hpp" #include "appstates/SaveCreateState.hpp" +#include "appstates/SaveImportState.hpp" #include "appstates/TaskState.hpp" #include "config/config.hpp" #include "data/data.hpp" @@ -28,7 +29,8 @@ namespace BACKUP_ALL, CREATE_SAVE, CREATE_ALL_SAVE, - DELETE_ALL_SAVE + DELETE_ALL_SAVE, + IMPORT_SAVE_DATA }; } // namespace @@ -64,10 +66,11 @@ void UserOptionState::update() switch (selected) { - case BACKUP_ALL: UserOptionState::backup_all(); break; - case CREATE_SAVE: UserOptionState::create_save_create(); break; - case CREATE_ALL_SAVE: UserOptionState::create_all_save_data(); break; - case DELETE_ALL_SAVE: UserOptionState::delete_all_save_data(); break; + case BACKUP_ALL: UserOptionState::backup_all(); break; + case CREATE_SAVE: UserOptionState::create_save_create(); break; + case CREATE_ALL_SAVE: UserOptionState::create_all_save_data(); break; + case DELETE_ALL_SAVE: UserOptionState::delete_all_save_data(); break; + case IMPORT_SAVE_DATA: UserOptionState::create_push_save_import(); break; } } else if (bPressed) { sm_menuPanel->close(); } @@ -159,6 +162,15 @@ void UserOptionState::delete_all_save_data() ConfirmTask::create_push_fade(queryString, true, tasks::useroptions::delete_all_save_data_for_user, m_dataStruct); } +void UserOptionState::create_push_save_import() +{ + const fslib::Path saveDirPath{config::get_working_directory() / "saves"}; + if (!fs::directory_has_contents(saveDirPath)) { return; } + + sm_menuPanel->hide(); + SaveImportState::create_and_push(m_user); +} + void UserOptionState::deactivate_state() { sm_menuPanel->clear_elements(); diff --git a/source/logging/logger.cpp b/source/logging/logger.cpp index 6b2f35e..abadebf 100644 --- a/source/logging/logger.cpp +++ b/source/logging/logger.cpp @@ -33,6 +33,7 @@ void logger::initialize() void logger::log(const char *format, ...) noexcept { static std::mutex logLock{}; + static const fslib::Path logPath{PATH_JKSV_LOG}; std::array vaBuffer = {0}; std::va_list vaList{}; @@ -46,7 +47,6 @@ void logger::log(const char *format, ...) noexcept std::strftime(timeDateBuffer.data(), TIME_BUFFER, "[%c] ", &localTime); std::lock_guard logGuard(logLock); - const fslib::Path logPath{PATH_JKSV_LOG}; fslib::File logFile(logPath, FsOpenMode_Append); if (logFile.is_open() && logFile.get_size() >= SIZE_LOG_LIMIT) { From 9d542b431417cdde5d545945fee58fbfb9981a13 Mon Sep 17 00:00:00 2001 From: J-D-K Date: Wed, 24 Sep 2025 15:32:57 -0400 Subject: [PATCH 2/4] SaveImportState progress. --- include/appstates/SaveImportState.hpp | 21 +++++- include/fs/SaveMetaData.hpp | 15 ++-- include/fs/save_data_functions.hpp | 4 + include/tasks/saveimport.hpp | 8 ++ source/appstates/SaveImportState.cpp | 27 ++++++- source/fs/SaveMetaData.cpp | 29 +++---- source/fs/save_data_functions.cpp | 29 ++++++- source/tasks/backup.cpp | 19 +++-- source/tasks/saveimport.cpp | 104 ++++++++++++++++++++++++++ 9 files changed, 222 insertions(+), 34 deletions(-) create mode 100644 include/tasks/saveimport.hpp create mode 100644 source/tasks/saveimport.cpp diff --git a/include/appstates/SaveImportState.hpp b/include/appstates/SaveImportState.hpp index 5957691..3b30a8c 100644 --- a/include/appstates/SaveImportState.hpp +++ b/include/appstates/SaveImportState.hpp @@ -3,6 +3,7 @@ #include "appstates/BaseState.hpp" #include "data/data.hpp" #include "fslib.hpp" +#include "sys/sys.hpp" #include "ui/ui.hpp" #include @@ -29,13 +30,28 @@ class SaveImportState final : public BaseState void render() override; + // clang-format off + // Struct used to pass data to the task. + struct DataStruct : sys::Task::DataStruct + { + data::User *user{}; + fslib::Path path{}; + }; + // clang-format on + + /// @brief Makes things easier to read and type. + using TaskData = std::shared_ptr; + private: /// @brief Pointer to the target user. data::User *m_user{}; - /// @brief + /// @brief Directory listing of the saves directory. fslib::Directory m_saveDir{}; + /// @brief This is used to pass data to the task function. + static inline SaveImportState::TaskData sm_taskData{}; + /// @brief Menu shared by all instances. static inline std::shared_ptr sm_saveMenu{}; @@ -48,6 +64,9 @@ class SaveImportState final : public BaseState /// @brief Opens and void initialize_directory_menu(); + /// @brief Imports a backup selected from the menu. + void import_backup(); + /// @brief Cleans up and deactivates the state. void deactivate_state(); }; diff --git a/include/fs/SaveMetaData.hpp b/include/fs/SaveMetaData.hpp index 359a0fd..b337a91 100644 --- a/include/fs/SaveMetaData.hpp +++ b/include/fs/SaveMetaData.hpp @@ -6,12 +6,6 @@ namespace fs { - /// @brief This is the magic value written to the beginning. - constexpr uint32_t SAVE_META_MAGIC = 0x56534B4A; - - /// @brief This is the filename used for the save data meta info. - static constexpr std::string_view NAME_SAVE_META = ".nx_save_meta.bin"; - // clang-format off struct SaveMetaData { @@ -23,6 +17,7 @@ namespace fs uint8_t saveDataType{}; uint8_t saveDataRank{}; uint16_t saveDataIndex{}; + uint8_t saveDataSpaceID{}; uint64_t ownerID{}; uint64_t timestamp{}; uint32_t flags{}; @@ -32,6 +27,14 @@ namespace fs } __attribute__((packed)); // clang-format on + /// @brief This is the magic value written to the beginning. + constexpr uint32_t SAVE_META_MAGIC = 0x56534B4A; + + /// @brief This is the filename used for the save data meta info. + inline constexpr std::string_view NAME_SAVE_META = ".nx_save_meta.bin"; + + inline constexpr ssize_t SIZE_SAVE_META = sizeof(fs::SaveMetaData); + /// @brief Didn't feel like a whole new file just for this. Fills an fs::SaveMetaData struct. bool fill_save_meta_data(const FsSaveDataInfo *saveInfo, SaveMetaData &meta) noexcept; diff --git a/include/fs/save_data_functions.hpp b/include/fs/save_data_functions.hpp index d452b7f..48086d1 100644 --- a/include/fs/save_data_functions.hpp +++ b/include/fs/save_data_functions.hpp @@ -1,5 +1,6 @@ #pragma once #include "data/data.hpp" +#include "fs/SaveMetaData.hpp" #include @@ -11,6 +12,9 @@ namespace fs /// @return True on success. False on failure. bool create_save_data_for(data::User *targetUser, data::TitleInfo *titleInfo) noexcept; + /// @brief Creates save data for the user passed using the meta data passed. + bool create_save_data_for(data::User *targetUser, const fs::SaveMetaData &saveMeta) noexcept; + /// @brief Deletes the save data of the FsSaveDataInfo passed. /// @param saveInfo Save data to delete. /// @return True on success. False on failure. diff --git a/include/tasks/saveimport.hpp b/include/tasks/saveimport.hpp new file mode 100644 index 0000000..790b1c6 --- /dev/null +++ b/include/tasks/saveimport.hpp @@ -0,0 +1,8 @@ +#pragma once +#include "sys/Task.hpp" + +namespace tasks::saveimport +{ + /// @brief Imports a random save backup according to the meta data. + void import_save_backup(sys::threadpool::JobData taskData); +} diff --git a/source/appstates/SaveImportState.cpp b/source/appstates/SaveImportState.cpp index dce9708..da2d495 100644 --- a/source/appstates/SaveImportState.cpp +++ b/source/appstates/SaveImportState.cpp @@ -1,7 +1,12 @@ #include "appstates/SaveImportState.hpp" +#include "appstates/BackupMenuState.hpp" +#include "appstates/ConfirmState.hpp" #include "config/config.hpp" #include "input.hpp" +#include "strings/strings.hpp" +#include "stringutil.hpp" +#include "tasks/saveimport.hpp" SaveImportState::SaveImportState(data::User *user) : m_user(user) @@ -21,7 +26,7 @@ void SaveImportState::update() const bool aPressed = input::button_pressed(HidNpadButton_A); const bool bPressed = input::button_pressed(HidNpadButton_B); - if (aPressed) {} + if (aPressed) { SaveImportState::import_backup(); } else if (bPressed) { sm_slidePanel->close(); } else if (sm_slidePanel->is_closed()) { SaveImportState::deactivate_state(); } } @@ -38,7 +43,9 @@ void SaveImportState::initialize_static_members() { static constexpr int SIZE_PANEL_WIDTH = 512; - if (sm_saveMenu && sm_slidePanel) { return; } + if (sm_taskData && sm_saveMenu && sm_slidePanel) { return; } + + sm_taskData = std::make_shared(); sm_saveMenu = ui::Menu::create(8, 8, 492, 22, 720); sm_slidePanel = ui::SlideOutPanel::create(SIZE_PANEL_WIDTH, ui::SlideOutPanel::Side::Right); @@ -56,6 +63,22 @@ void SaveImportState::initialize_directory_menu() for (const fslib::DirectoryEntry &entry : m_saveDir) { sm_saveMenu->add_option(entry.get_filename()); } } +void SaveImportState::import_backup() +{ + const int selected = sm_saveMenu->get_selected(); + const bool holdRequired = config::get_by_key(config::keys::HOLD_FOR_RESTORATION); + + const fslib::DirectoryEntry &target = m_saveDir[selected]; + const char *targetName = target.get_filename(); + sm_taskData->user = m_user; + sm_taskData->path = config::get_working_directory() / "saves" / targetName; + + const char *confirmFormat = strings::get_by_name(strings::names::BACKUPMENU_CONFS, 1); + std::string query = stringutil::get_formatted_string(confirmFormat, targetName); + + ConfirmProgress::create_and_push(query, holdRequired, tasks::saveimport::import_save_backup, sm_taskData); +} + void SaveImportState::deactivate_state() { sm_saveMenu->reset(); diff --git a/source/fs/SaveMetaData.cpp b/source/fs/SaveMetaData.cpp index fdd99aa..27d4c87 100644 --- a/source/fs/SaveMetaData.cpp +++ b/source/fs/SaveMetaData.cpp @@ -17,20 +17,21 @@ bool fs::fill_save_meta_data(const FsSaveDataInfo *saveInfo, fs::SaveMetaData &m const bool extraRead = fs::read_save_extra_data(saveInfo, extraData); if (!extraRead) { return false; } - meta = {.magic = fs::SAVE_META_MAGIC, - .revision = 0x00, - .applicationID = extraData.attr.application_id, - .accountID = extraData.attr.uid, - .systemSaveID = extraData.attr.system_save_data_id, - .saveDataType = extraData.attr.save_data_type, - .saveDataRank = extraData.attr.save_data_rank, - .saveDataIndex = extraData.attr.save_data_index, - .ownerID = extraData.owner_id, - .timestamp = extraData.timestamp, - .flags = extraData.flags, - .saveDataSize = extraData.data_size, - .journalSize = extraData.journal_size, - .commitID = extraData.commit_id}; + meta = {.magic = fs::SAVE_META_MAGIC, + .revision = 0x01, + .applicationID = extraData.attr.application_id, + .accountID = extraData.attr.uid, + .systemSaveID = extraData.attr.system_save_data_id, + .saveDataType = extraData.attr.save_data_type, + .saveDataRank = extraData.attr.save_data_rank, + .saveDataIndex = extraData.attr.save_data_index, + .saveDataSpaceID = saveInfo->save_data_space_id, + .ownerID = extraData.owner_id, + .timestamp = extraData.timestamp, + .flags = extraData.flags, + .saveDataSize = extraData.data_size, + .journalSize = extraData.journal_size, + .commitID = extraData.commit_id}; return true; } diff --git a/source/fs/save_data_functions.cpp b/source/fs/save_data_functions.cpp index 3953e55..dc1a720 100644 --- a/source/fs/save_data_functions.cpp +++ b/source/fs/save_data_functions.cpp @@ -3,9 +3,13 @@ #include "error.hpp" #include "logging/logger.hpp" +namespace +{ + inline constexpr FsSaveDataMetaInfo SAVE_CREATE_META = {.size = 0x40060, .type = FsSaveDataMetaType_Thumbnail}; +} + bool fs::create_save_data_for(data::User *targetUser, data::TitleInfo *titleInfo) noexcept { - static constexpr FsSaveDataMetaInfo saveMeta = {.size = 0x40060, .type = FsSaveDataMetaType_Thumbnail}; const uint8_t saveType = targetUser->get_account_save_type(); const uint64_t applicationID = titleInfo->get_application_id(); @@ -29,7 +33,26 @@ bool fs::create_save_data_for(data::User *targetUser, data::TitleInfo *titleInfo .save_data_space_id = FsSaveDataSpaceId_User}; // I want this recorded. - return error::libnx(fsCreateSaveDataFileSystem(&saveAttributes, &saveCreation, &saveMeta)) == false; + return error::libnx(fsCreateSaveDataFileSystem(&saveAttributes, &saveCreation, &SAVE_CREATE_META)) == false; +} + +bool fs::create_save_data_for(data::User *targetUser, const fs::SaveMetaData &saveMeta) noexcept +{ + const FsSaveDataAttribute saveAttributes = {.application_id = saveMeta.applicationID, + .uid = targetUser->get_account_id(), + .system_save_data_id = saveMeta.systemSaveID, + .save_data_type = saveMeta.saveDataType, + .save_data_rank = saveMeta.saveDataRank, + .save_data_index = saveMeta.saveDataIndex}; + + const FsSaveDataCreationInfo saveCreation = {.save_data_size = saveMeta.saveDataSize, + .journal_size = saveMeta.journalSize, + .available_size = 0x4000, + .owner_id = saveMeta.ownerID, + .flags = 0, + .save_data_space_id = saveMeta.saveDataSpaceID}; + + return error::libnx(fsCreateSaveDataFileSystem(&saveAttributes, &saveCreation, &SAVE_CREATE_META)) == false; } bool fs::delete_save_data(const FsSaveDataInfo *saveInfo) noexcept @@ -72,4 +95,4 @@ bool fs::read_save_extra_data(const FsSaveDataInfo *saveInfo, FsSaveDataExtraDat fsReadSaveDataFileSystemExtraDataBySaveDataSpaceId(&extraOut, EXTRA_SIZE, spaceID, saveInfo->save_data_id)); return !readError; -} \ No newline at end of file +} diff --git a/source/tasks/backup.cpp b/source/tasks/backup.cpp index 1274304..47f4829 100644 --- a/source/tasks/backup.cpp +++ b/source/tasks/backup.cpp @@ -15,7 +15,6 @@ namespace { constexpr const char *STRING_ZIP_EXT = ".zip"; constexpr const char *PATH_JKSV_TEMP = "sdmc:/jksvTemp.zip"; // This is named this so if something fails, people know. - constexpr size_t SIZE_SAVE_META = sizeof(fs::SaveMetaData); } // Definitions at bottom. @@ -267,8 +266,12 @@ void tasks::backup::restore_backup_local(sys::threadpool::JobData taskData) fs::copy_file_commit(target, fs::DEFAULT_SAVE_ROOT, journalSize, task); } - spawningState->save_data_written(); - spawningState->refresh(); + if (spawningState) + { + spawningState->save_data_written(); + spawningState->refresh(); + } + task->complete(); } @@ -575,7 +578,7 @@ static bool read_and_process_meta(const fslib::Path &targetDir, BackupMenuState: } fs::SaveMetaData metaData{}; - const bool metaRead = metaFile.read(&metaData, SIZE_SAVE_META) == SIZE_SAVE_META; + const bool metaRead = metaFile.read(&metaData, fs::SIZE_SAVE_META) <= fs::SIZE_SAVE_META; const bool processed = metaRead && fs::process_save_meta_data(saveInfo, metaData); if (!metaRead || !processed) { @@ -618,7 +621,7 @@ static bool read_and_process_meta(fs::MiniUnzip &unzip, BackupMenuState::TaskDat return false; } - const bool metaRead = metaFound && unzip.read(&saveMeta, SIZE_SAVE_META) == SIZE_SAVE_META; + const bool metaRead = metaFound && unzip.read(&saveMeta, fs::SIZE_SAVE_META) <= fs::SIZE_SAVE_META; const bool metaProcessed = metaRead && fs::process_save_meta_data(saveInfo, saveMeta); if (!metaRead || !metaProcessed) { @@ -638,14 +641,14 @@ static void write_meta_file(const fslib::Path &target, const FsSaveDataInfo *sav fs::SaveMetaData saveMeta{}; const fslib::Path metaPath{target / fs::NAME_SAVE_META}; const bool hasMeta = fs::fill_save_meta_data(saveInfo, saveMeta); - fslib::File metaFile{metaPath, FsOpenMode_Create | FsOpenMode_Write, SIZE_SAVE_META}; + fslib::File metaFile{metaPath, FsOpenMode_Create | FsOpenMode_Write, fs::SIZE_SAVE_META}; if (!metaFile.is_open() || !hasMeta) { ui::PopMessageManager::push_message(popTicks, popErrorWritingMeta); return; } - const bool metaWritten = metaFile.write(&saveMeta, SIZE_SAVE_META) == SIZE_SAVE_META; + const bool metaWritten = metaFile.write(&saveMeta, fs::SIZE_SAVE_META) <= fs::SIZE_SAVE_META; if (!metaWritten) { ui::PopMessageManager::push_message(popTicks, popErrorWritingMeta); } } @@ -656,7 +659,7 @@ static void write_meta_zip(fs::MiniZip &zip, const FsSaveDataInfo *saveInfo) fs::SaveMetaData saveMeta{}; const bool hasMeta = fs::fill_save_meta_data(saveInfo, saveMeta); const bool openMeta = hasMeta && zip.open_new_file(fs::NAME_SAVE_META); - const bool writeMeta = openMeta && zip.write(&saveMeta, SIZE_SAVE_META); + const bool writeMeta = openMeta && zip.write(&saveMeta, fs::SIZE_SAVE_META); const bool closeMeta = openMeta && zip.close_current_file(); if (hasMeta && (!openMeta || !writeMeta || !closeMeta)) { diff --git a/source/tasks/saveimport.cpp b/source/tasks/saveimport.cpp new file mode 100644 index 0000000..c2f961f --- /dev/null +++ b/source/tasks/saveimport.cpp @@ -0,0 +1,104 @@ +#include "tasks/saveimport.hpp" + +#include "appstates/SaveImportState.hpp" +#include "error.hpp" +#include "fs/fs.hpp" +#include "fslib.hpp" +#include "strings/strings.hpp" +#include "tasks/backup.hpp" +#include "ui/PopMessageManager.hpp" + +// Defined at bottom. +static bool read_save_meta(const fslib::Path &path, fs::SaveMetaData &metaOut); +static bool test_for_save(data::User *user, const fs::SaveMetaData &saveMeta); + +void tasks::saveimport::import_save_backup(sys::threadpool::JobData taskData) +{ + auto castData = std::static_pointer_cast(taskData); + + sys::ProgressTask *task = static_cast(castData->task); + if (error::is_null(task)) { return; } + + data::User *user = castData->user; + const fslib::Path &target = castData->path; + + fs::SaveMetaData metaData{}; + const bool metaRead = read_save_meta(target, metaData); + if (!metaRead) { TASK_FINISH_RETURN(task); } + + const bool saveExists = test_for_save(user, metaData); + if (!saveExists) { fs::create_save_data_for(user, metaData); } +} + +static bool read_save_meta(const fslib::Path &path, fs::SaveMetaData &metaOut) +{ + const bool isDir = fslib::directory_exists(path); + if (isDir) + { + const fslib::Path metaPath{path / fs::NAME_SAVE_META}; + const bool exists = fslib::file_exists(metaPath); + if (!exists) { return false; } + + fslib::File metaFile{metaPath, FsOpenMode_Read}; + const bool metaRead = metaFile.is_open() && metaFile.get_size() <= fs::SIZE_SAVE_META && + metaFile.read(&metaOut, fs::SIZE_SAVE_META) <= fs::SIZE_SAVE_META; + const bool magicMatch = metaRead && metaOut.magic == fs::SAVE_META_MAGIC; + if (!metaRead || !magicMatch) { return false; } + } + else + { + fs::MiniUnzip zip{path}; + if (!zip.is_open()) { return false; } + + const bool located = zip.locate_file(fs::NAME_SAVE_META); + const bool sizeMatch = located && zip.get_uncompressed_size() <= fs::SIZE_SAVE_META; + const bool metaRead = sizeMatch && zip.read(&metaOut, fs::SIZE_SAVE_META) <= fs::SIZE_SAVE_META; + const bool magicMatch = metaRead && metaOut.magic == fs::SAVE_META_MAGIC; + if (!metaRead || !magicMatch) { return false; } + } + + return true; +} + +static bool test_for_save(data::User *user, const fs::SaveMetaData &saveMeta) +{ + const FsSaveDataType saveType = user->get_account_save_type(); + + bool mounted{}; + switch (saveType) + { + case FsSaveDataType_Account: + { + mounted = + fslib::open_account_save_file_system(fs::DEFAULT_SAVE_MOUNT, saveMeta.applicationID, user->get_account_id()); + } + break; + + case FsSaveDataType_Bcat: + { + mounted = fslib::open_bcat_save_file_system(fs::DEFAULT_SAVE_MOUNT, saveMeta.applicationID); + } + break; + + case FsSaveDataType_Device: + { + mounted = fslib::open_device_save_file_system(fs::DEFAULT_SAVE_MOUNT, saveMeta.applicationID); + } + break; + + case FsSaveDataType_Cache: + { + mounted = fslib::open_cache_save_file_system(fs::DEFAULT_SAVE_MOUNT, + saveMeta.applicationID, + saveMeta.saveDataIndex, + static_cast(saveMeta.saveDataIndex)); + } + break; + + default: return false; + } + + if (mounted) { fslib::close_file_system(fs::DEFAULT_SAVE_MOUNT); } + + return mounted; +} From cdc69372491951698dd8fddd4a585e46c352f81b Mon Sep 17 00:00:00 2001 From: J-D-K Date: Wed, 24 Sep 2025 19:05:56 -0400 Subject: [PATCH 3/4] Finish SaveImportState --- include/fs/SaveMetaData.hpp | 2 +- source/fs/SaveMetaData.cpp | 7 +++++-- source/tasks/saveimport.cpp | 39 ++++++++++++++++++++++++++++++++++--- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/include/fs/SaveMetaData.hpp b/include/fs/SaveMetaData.hpp index b337a91..2f80ec5 100644 --- a/include/fs/SaveMetaData.hpp +++ b/include/fs/SaveMetaData.hpp @@ -17,13 +17,13 @@ namespace fs uint8_t saveDataType{}; uint8_t saveDataRank{}; uint16_t saveDataIndex{}; - uint8_t saveDataSpaceID{}; uint64_t ownerID{}; uint64_t timestamp{}; uint32_t flags{}; int64_t saveDataSize{}; int64_t journalSize{}; uint64_t commitID{}; + uint8_t saveDataSpaceID{}; } __attribute__((packed)); // clang-format on diff --git a/source/fs/SaveMetaData.cpp b/source/fs/SaveMetaData.cpp index 27d4c87..873fc1e 100644 --- a/source/fs/SaveMetaData.cpp +++ b/source/fs/SaveMetaData.cpp @@ -5,6 +5,7 @@ #include "fs/save_data_functions.hpp" #include "fs/save_mount.hpp" #include "fslib.hpp" +#include "logging/logger.hpp" namespace { @@ -25,13 +26,13 @@ bool fs::fill_save_meta_data(const FsSaveDataInfo *saveInfo, fs::SaveMetaData &m .saveDataType = extraData.attr.save_data_type, .saveDataRank = extraData.attr.save_data_rank, .saveDataIndex = extraData.attr.save_data_index, - .saveDataSpaceID = saveInfo->save_data_space_id, .ownerID = extraData.owner_id, .timestamp = extraData.timestamp, .flags = extraData.flags, .saveDataSize = extraData.data_size, .journalSize = extraData.journal_size, - .commitID = extraData.commit_id}; + .commitID = extraData.commit_id, + .saveDataSpaceID = saveInfo->save_data_space_id}; return true; } @@ -42,6 +43,8 @@ bool fs::process_save_meta_data(const FsSaveDataInfo *saveInfo, const SaveMetaDa const bool extraRead = fs::read_save_extra_data(saveInfo, extraData); if (!extraRead) { return false; } + logger::log("size: %X, %X", extraData.data_size, meta.saveDataSize); + const bool needsExtend = extraData.data_size < meta.saveDataSize; const bool extended = needsExtend && fs::extend_save_data(saveInfo, meta.saveDataSize, meta.journalSize); if (needsExtend && !extended) { return false; } diff --git a/source/tasks/saveimport.cpp b/source/tasks/saveimport.cpp index c2f961f..86e4c79 100644 --- a/source/tasks/saveimport.cpp +++ b/source/tasks/saveimport.cpp @@ -1,11 +1,12 @@ #include "tasks/saveimport.hpp" +#include "appstates/MainMenuState.hpp" #include "appstates/SaveImportState.hpp" #include "error.hpp" #include "fs/fs.hpp" #include "fslib.hpp" #include "strings/strings.hpp" -#include "tasks/backup.hpp" +#include "stringutil.hpp" #include "ui/PopMessageManager.hpp" // Defined at bottom. @@ -24,10 +25,42 @@ void tasks::saveimport::import_save_backup(sys::threadpool::JobData taskData) fs::SaveMetaData metaData{}; const bool metaRead = read_save_meta(target, metaData); - if (!metaRead) { TASK_FINISH_RETURN(task); } + if (!metaRead || metaData.revision < 1) { TASK_FINISH_RETURN(task); } const bool saveExists = test_for_save(user, metaData); - if (!saveExists) { fs::create_save_data_for(user, metaData); } + if (!saveExists) + { + // Gonna borrow this. + const char *statusFormat = strings::get_by_name(strings::names::USEROPTION_STATUS, 0); + const std::string hexID = stringutil::get_formatted_string("%016llX", metaData.applicationID); + std::string status = stringutil::get_formatted_string(statusFormat, hexID.c_str()); + task->set_status(status); + + fs::create_save_data_for(user, metaData); + user->clear_data_entries(); + user->load_user_data(); + MainMenuState::refresh_view_states(); + } + + const FsSaveDataInfo *saveInfo = user->get_save_info_by_id(metaData.applicationID); + if (error::is_null(saveInfo)) { TASK_FINISH_RETURN(task); } + + { + fs::ScopedSaveMount saveMount{fs::DEFAULT_SAVE_MOUNT, saveInfo}; + if (!saveMount.is_open()) { TASK_FINISH_RETURN(task); } + + const bool isDir = fslib::directory_exists(target); + if (isDir) { fs::copy_directory_commit(target, fs::DEFAULT_SAVE_ROOT, metaData.journalSize, task); } + else + { + fs::MiniUnzip unzip{target}; + if (!unzip.is_open()) { TASK_FINISH_RETURN(task); } + + fs::copy_zip_to_directory(unzip, fs::DEFAULT_SAVE_ROOT, metaData.journalSize, task); + } + } + + task->complete(); } static bool read_save_meta(const fslib::Path &path, fs::SaveMetaData &metaOut) From 29fc92b6b9193fd0821b96b131dc5e320629c579 Mon Sep 17 00:00:00 2001 From: J-D-K Date: Wed, 24 Sep 2025 19:15:07 -0400 Subject: [PATCH 4/4] Update strings. --- romfs/Text/DE.json | 3 ++- romfs/Text/ENGB.json | 3 ++- romfs/Text/ES.json | 3 ++- romfs/Text/ES419.json | 3 ++- romfs/Text/FR.json | 3 ++- romfs/Text/FRCA.json | 3 ++- romfs/Text/IT.json | 3 ++- romfs/Text/JA.json | 3 ++- romfs/Text/KO.json | 5 +++-- romfs/Text/NL.json | 3 ++- romfs/Text/PT.json | 3 ++- romfs/Text/PTBR.json | 3 ++- romfs/Text/RU.json | 3 ++- romfs/Text/ZHCN.json | 3 ++- romfs/Text/ZHTW.json | 3 ++- source/fs/SaveMetaData.cpp | 2 -- 16 files changed, 31 insertions(+), 18 deletions(-) diff --git a/romfs/Text/DE.json b/romfs/Text/DE.json index a5eeb33..08ce31d 100644 --- a/romfs/Text/DE.json +++ b/romfs/Text/DE.json @@ -278,7 +278,8 @@ "0: Alle für `%s` dumpen", "1: Speicherdaten für `%s` erstellen", "2: Alle Speicherdaten für `%s` erstellen", - "3: Alle Speicherdaten für `%s` löschen" + "3: Alle Speicherdaten für `%s` löschen", + "4: Speicherdaten für `%s` importieren" ], "WebDavStrings": [ "0: WebDav erfolgreich gestartet!", diff --git a/romfs/Text/ENGB.json b/romfs/Text/ENGB.json index 5c8d17a..6124fab 100644 --- a/romfs/Text/ENGB.json +++ b/romfs/Text/ENGB.json @@ -277,7 +277,8 @@ "0: Dump all for `%s`", "1: Create Save Data for `%s`", "2: Create All Save Data for `%s`", - "3: Delete All Save Data for `%s`" + "3: Delete All Save Data for `%s`", + "4: Import Save Data for `%s`" ], "WebDavStrings": [ "0: WebDav successfully started!", diff --git a/romfs/Text/ES.json b/romfs/Text/ES.json index 1c7f55a..ab70055 100644 --- a/romfs/Text/ES.json +++ b/romfs/Text/ES.json @@ -278,7 +278,8 @@ "0: Volcar todo para `%s`", "1: Crear datos guardados para `%s`", "2: Crear todos los datos guardados para `%s`", - "3: Eliminar todos los datos guardados para `%s`" + "3: Eliminar todos los datos guardados para `%s`", + "4: Importar datos guardados de `%s`" ], "WebDavStrings": [ "0: ¡WebDav iniciado correctamente!", diff --git a/romfs/Text/ES419.json b/romfs/Text/ES419.json index 12e7f7f..f7111e7 100644 --- a/romfs/Text/ES419.json +++ b/romfs/Text/ES419.json @@ -278,7 +278,8 @@ "0: Volcar todo para `%s`", "1: Crear datos guardados para `%s`", "2: Crear todos los datos guardados para `%s`", - "3: Eliminar todos los datos guardados para `%s`" + "3: Eliminar todos los datos guardados para `%s`", + "4: Importar datos guardados de `%s`" ], "WebDavStrings": [ "0: ¡WebDav iniciado correctamente!", diff --git a/romfs/Text/FR.json b/romfs/Text/FR.json index 224ff53..f829bc6 100644 --- a/romfs/Text/FR.json +++ b/romfs/Text/FR.json @@ -278,7 +278,8 @@ "0: Exporter tout pour `%s`", "1: Créer des données sauvegardées pour `%s`", "2: Créer toutes les données sauvegardées pour `%s`", - "3: Supprimer toutes les données sauvegardées pour `%s`" + "3: Supprimer toutes les données sauvegardées pour `%s`", + "4: Importer les données de sauvegarde pour `%s`" ], "WebDavStrings": [ "0: WebDav démarré avec succès !", diff --git a/romfs/Text/FRCA.json b/romfs/Text/FRCA.json index 4fde211..3aa85c6 100644 --- a/romfs/Text/FRCA.json +++ b/romfs/Text/FRCA.json @@ -278,7 +278,8 @@ "0: Exporter tout pour `%s`", "1: Créer des données sauvegardées pour `%s`", "2: Créer toutes les données sauvegardées pour `%s`", - "3: Supprimer toutes les données sauvegardées pour `%s`" + "3: Supprimer toutes les données sauvegardées pour `%s`", + "4: Importer les données de sauvegarde pour `%s`" ], "WebDavStrings": [ "0: WebDav démarré avec succès !", diff --git a/romfs/Text/IT.json b/romfs/Text/IT.json index c74a422..b2866be 100644 --- a/romfs/Text/IT.json +++ b/romfs/Text/IT.json @@ -278,7 +278,8 @@ "0: Esporta tutto per `%s`", "1: Crea dati di salvataggio per `%s`", "2: Crea tutti i dati di salvataggio per `%s`", - "3: Elimina tutti i dati di salvataggio per `%s`" + "3: Elimina tutti i dati di salvataggio per `%s`", + "4: Importa i dati di salvataggio per `%s`" ], "WebDavStrings": [ "0: WebDav avviato con successo!", diff --git a/romfs/Text/JA.json b/romfs/Text/JA.json index 874b764..cd5c56d 100644 --- a/romfs/Text/JA.json +++ b/romfs/Text/JA.json @@ -278,7 +278,8 @@ "0: `%s` の すべてを ダンプ", "1: `%s` の セーブデータを 作成", "2: `%s` の すべての セーブデータを 作成", - "3: `%s` の すべての セーブデータを 削除" + "3: `%s` の すべての セーブデータを 削除", + "4: `%s` のセーブデータをインポート" ], "WebDavStrings": [ "0: WebDav が 正常に 開始されました!", diff --git a/romfs/Text/KO.json b/romfs/Text/KO.json index f58e0ed..959a366 100644 --- a/romfs/Text/KO.json +++ b/romfs/Text/KO.json @@ -278,7 +278,8 @@ "0: `%s` 의 모든 데이터 덤프", "1: `%s` 의 저장 데이터 생성", "2: `%s` 의 모든 저장 데이터 생성", - "3: `%s` 의 모든 저장 데이터 삭제" + "3: `%s` 의 모든 저장 데이터 삭제", + "4: `%s` 의 세이브 데이터를 가져오기" ], "WebDavStrings": [ "0: WebDav 가 성공적으로 시작되었습니다!", @@ -289,4 +290,4 @@ "1: 아니요 [B]", "2: 확인 [A]" ] -} +} \ No newline at end of file diff --git a/romfs/Text/NL.json b/romfs/Text/NL.json index 77254cb..9e41e31 100644 --- a/romfs/Text/NL.json +++ b/romfs/Text/NL.json @@ -278,7 +278,8 @@ "0: Dump alles voor `%s`", "1: Maak opslaggegevens aan voor `%s`", "2: Maak alle opslaggegevens aan voor `%s`", - "3: Verwijder alle opslaggegevens voor `%s`" + "3: Verwijder alle opslaggegevens voor `%s`", + "4: Savegegevens importeren voor `%s`" ], "WebDavStrings": [ "0: WebDav succesvol gestart!", diff --git a/romfs/Text/PT.json b/romfs/Text/PT.json index dce8a3c..bbcdec0 100644 --- a/romfs/Text/PT.json +++ b/romfs/Text/PT.json @@ -278,7 +278,8 @@ "0: Despejar tudo para `%s`", "1: Criar dados guardados para `%s`", "2: Criar todos os dados guardados para `%s`", - "3: Eliminar todos os dados guardados para `%s`" + "3: Eliminar todos os dados guardados para `%s`", + "4: Importar dados de salvamento para `%s`" ], "WebDavStrings": [ "0: WebDav iniciado com sucesso!", diff --git a/romfs/Text/PTBR.json b/romfs/Text/PTBR.json index 2dac5e3..e6619bf 100644 --- a/romfs/Text/PTBR.json +++ b/romfs/Text/PTBR.json @@ -278,7 +278,8 @@ "0: Descartar tudo para `%s`", "1: Criar dados salvos para `%s`", "2: Criar todos os dados salvos para `%s`", - "3: Deletar todos os dados salvos para `%s`" + "3: Deletar todos os dados salvos para `%s`", + "4: Importar dados de salvamento para `%s`" ], "WebDavStrings": [ "0: WebDav iniciado com sucesso!", diff --git a/romfs/Text/RU.json b/romfs/Text/RU.json index 089f6cf..80e7a70 100644 --- a/romfs/Text/RU.json +++ b/romfs/Text/RU.json @@ -278,7 +278,8 @@ "0: Сбросить всё для `%s`", "1: Создать данные сохранения для `%s`", "2: Создать все данные сохранения для `%s`", - "3: Удалить все данные сохранения для `%s`" + "3: Удалить все данные сохранения для `%s`", + "4: Импортировать сохранение для `%s`" ], "WebDavStrings": [ "0: WebDav успешно запущен!", diff --git a/romfs/Text/ZHCN.json b/romfs/Text/ZHCN.json index 6acf0c6..49a0586 100644 --- a/romfs/Text/ZHCN.json +++ b/romfs/Text/ZHCN.json @@ -278,7 +278,8 @@ "0: 导出 `%s` 的所有存档", "1: 为 `%s` 创建存档数据", "2: 为 `%s` 创建所有存档数据", - "3: 删除 `%s` 的所有存档数据" + "3: 删除 `%s` 的所有存档数据", + "4: 导入 `%s` 的存档数据" ], "WebDavStrings": [ "0: WebDav 启动成功!", diff --git a/romfs/Text/ZHTW.json b/romfs/Text/ZHTW.json index 66c19f8..0277693 100644 --- a/romfs/Text/ZHTW.json +++ b/romfs/Text/ZHTW.json @@ -278,7 +278,8 @@ "0: 匯出 `%s` 的所有存檔", "1: 為 `%s` 建立存檔資料", "2: 為 `%s` 建立所有存檔資料", - "3: 刪除 `%s` 的所有存檔資料" + "3: 刪除 `%s` 的所有存檔資料", + "4: 匯入 `%s` 的存檔資料" ], "WebDavStrings": [ "0: WebDav 啟動成功!", diff --git a/source/fs/SaveMetaData.cpp b/source/fs/SaveMetaData.cpp index 873fc1e..ed222e4 100644 --- a/source/fs/SaveMetaData.cpp +++ b/source/fs/SaveMetaData.cpp @@ -43,8 +43,6 @@ bool fs::process_save_meta_data(const FsSaveDataInfo *saveInfo, const SaveMetaDa const bool extraRead = fs::read_save_extra_data(saveInfo, extraData); if (!extraRead) { return false; } - logger::log("size: %X, %X", extraData.data_size, meta.saveDataSize); - const bool needsExtend = extraData.data_size < meta.saveDataSize; const bool extended = needsExtend && fs::extend_save_data(saveInfo, meta.saveDataSize, meta.journalSize); if (needsExtend && !extended) { return false; }