From 77ed5e6d0c2cbc24eebd83cb41a84a8a8f30aa46 Mon Sep 17 00:00:00 2001 From: OverkillLabs <10800190+OverkillLabs@users.noreply.github.com> Date: Mon, 28 Sep 2026 12:48:09 +0300 Subject: [PATCH] feat(savedata): mount aliasing, wildcard search, memory slots, async events Rework the local SaveData implementation into a general-purpose, title-agnostic backend used by both the native library and the standard libSceSaveData path: - Directory aliasing: long save directory names are hashed into short mount point aliases (persisted to disk) so they fit the SDK's fixed-size mount point buffer, with the aliases reversed on directory search. - Wildcard directory-name search (%/_ patterns) with sorting and optional reverse order, matching the DirNameSearch contract. - Save-data "memory" slots: small per-user, per-slot blobs the title can set up, read and write without mounting a directory, backed by files under the save root with a companion parameter blob, size and range validation on every access, and a dirty flag consulted on commit. - Async-flavored operations (backup, commit, memory sync) that queue SaveDataEvent records consumed through GetEventResult and can be bound to kernel event flags, plus transaction-resource and cancel/abort bookkeeping. - Atomic file writes (temp file + rename) so a mid-write crash never leaves a torn save on disk, and corrected/duplicated SDK error code constants. - Local storage app-status get/set/delete and transferring-mount support for PS4-to-PS5 style save transfers. The native library keeps its own lighter-weight mount/memory implementation (short "/_sm/" mount point aliases via the path-alias table) while gaining the same per-user memory-slot separation, atomic writes and bounds checking; the two implementations were merged by hand where upstream had independently touched the native Export.cpp in the same area (backup/event queue, short mount points) to keep both sets of improvements. None of this is game-specific: it is a general local save-data backend that works for any title and tolerates any user id. --- .../libs/prx/libSceSaveData.native/Export.cpp | 254 +++- .../prx/libSceSaveData.native/SaveData.hpp | 17 +- core/libs/prx/libSceSaveData/Export.cpp | 107 +- core/libs/prx/libSceSaveData/SaveDataCore.hpp | 1354 +++++++++++++++++ core/libs/prx/libSceSaveDataDialog/Export.cpp | 46 +- 5 files changed, 1658 insertions(+), 120 deletions(-) create mode 100644 core/libs/prx/libSceSaveData/SaveDataCore.hpp diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index ce6f82d14..6e1a7d4cc 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -24,24 +24,11 @@ static std::string save_root() { return std::string(SAVE_DIR); } -// Events the title polls with sceSaveDataGetEventResult (a backup completes at once here), and the -// "save data memory" blobs (small per-slot memories the title reads and writes without mounting), -// persisted as files under the save root so they survive restarts. +// Events the title polls with sceSaveDataGetEventResult; a backup completes at once here. static std::mutex g_state_mutex; static std::deque g_events; -static std::map> g_memories; constexpr std::uint32_t SAVE_DATA_EVENT_TYPE_BACKUP = 2; -static std::string memory_path(std::uint32_t slot) { - return save_root() + "/_memory_" + std::to_string(slot) + ".bin"; -} - -static void store_memory(std::uint32_t slot, const std::vector& memory) { - std::filesystem::create_directories(save_root()); - std::ofstream file(memory_path(slot), std::ios::binary | std::ios::trunc); - file.write(reinterpret_cast(memory.data()), static_cast(memory.size())); -} - static bool dir_name_match(const char* str, const char* pattern) { if (pattern == nullptr || pattern[0] == '\0') { return true; @@ -72,6 +59,78 @@ static bool dir_name_match(const char* str, const char* pattern) { return *str == '\0' && *pattern == '\0'; } +namespace { + +// Save-data memory: one blob per user and slot, kept next to the save directory (_sd_mem/u/slot.bin), +// with a small .param sidecar holding the last SaveDataParam the title wrote. Keying by user as well as slot +// keeps two users' in-memory saves from colliding on the same console. +constexpr char MEM_DIR[] = "_sd_mem"; +constexpr std::size_t MEM_MAX_SIZE = 0x1000000; // 16 MiB +std::mutex g_mem_mutex; + +std::string mem_path(std::int32_t user_id, std::uint32_t slot, const char* ext) { + char buf[96]; + std::snprintf(buf, sizeof(buf), "%s/u%d/slot%u.%s", MEM_DIR, static_cast(user_id), static_cast(slot), ext); + return buf; +} + +bool file_size_of(const std::string& path, std::size_t* out) { + std::error_code ec; + if (!std::filesystem::is_regular_file(path, ec)) { + return false; + } + const auto sz = std::filesystem::file_size(path, ec); + if (ec) { + return false; + } + *out = static_cast(sz); + return true; +} + +// Atomic-ish write: write to a temp file then rename over the target, so a kill mid-write never +// leaves a torn save behind. +bool write_file_replace(const std::string& path, const std::vector& data) { + const std::string tmp = path + ".tmp"; + { + std::ofstream f(tmp, std::ios::binary | std::ios::trunc); + if (!f) { + return false; + } + if (!data.empty()) { + f.write(data.data(), static_cast(data.size())); + } + f.flush(); + if (!f) { + return false; + } + } + std::error_code ec; + std::filesystem::rename(tmp, path, ec); + if (ec) { + std::filesystem::remove(path, ec); + ec.clear(); + std::filesystem::rename(tmp, path, ec); + } + return !ec; +} + +bool read_file_all(const std::string& path, std::vector& out) { + std::ifstream f(path, std::ios::binary); + if (!f) { + return false; + } + f.seekg(0, std::ios::end); + const auto n = static_cast(f.tellg()); + f.seekg(0); + out.resize(n); + if (n != 0) { + f.read(out.data(), static_cast(n)); + } + return static_cast(f); +} + +} // namespace + extern "C" { int APS5_VABI sceSaveDataBackup(const SaveDataBackup* backup) { @@ -196,29 +255,47 @@ int APS5_VABI sceSaveDataGetParam(const SaveDataMountPoint* mount_point, uint32_ int APS5_VABI sceSaveDataGetSaveDataMemory2(SaveDataMemoryGet2* get_param) { if (get_param == nullptr) { - throw std::runtime_error("sceSaveDataGetSaveDataMemory2: null argument"); + return SAVE_DATA_ERROR_PARAMETER; } - std::lock_guard lock(g_state_mutex); - const auto it = g_memories.find(get_param->slot_id); - if (it == g_memories.end()) { - return SAVE_DATA_ERROR_NOT_FOUND; + if (!g_initialized) { + return SAVE_DATA_ERROR_NOT_INITIALIZED; } - if (get_param->data != nullptr && get_param->data->buf != nullptr) { - const auto& memory = it->second; - if (get_param->data->offset > memory.size()) { + std::lock_guard lk(g_mem_mutex); + const std::string path = mem_path(get_param->user_id, get_param->slot_id, "bin"); + std::size_t size = 0; + if (!file_size_of(path, &size)) { + return SAVE_DATA_ERROR_MEMORY_NOT_READY; + } + const SaveDataMemoryData* d = get_param->data; + if (d != nullptr && d->buf_size != 0) { + if (d->buf == nullptr || d->offset > size || d->buf_size > size - d->offset) { return SAVE_DATA_ERROR_PARAMETER; } - const auto bytes = std::min(get_param->data->buf_size, memory.size() - get_param->data->offset); - std::memcpy(get_param->data->buf, memory.data() + get_param->data->offset, bytes); + std::ifstream f(path, std::ios::binary); + if (!f) { + return SAVE_DATA_ERROR_INTERNAL; + } + f.seekg(static_cast(d->offset)); + f.read(static_cast(d->buf), static_cast(d->buf_size)); + if (!f) { + return SAVE_DATA_ERROR_INTERNAL; + } + } + if (get_param->param != nullptr) { + std::memset(get_param->param, 0, sizeof(SaveDataParam)); + std::vector pd; + if (read_file_all(mem_path(get_param->user_id, get_param->slot_id, "param"), pd)) { + std::memcpy(get_param->param, pd.data(), std::min(pd.size(), sizeof(SaveDataParam))); + } + } + if (get_param->icon != nullptr) { + get_param->icon->data_size = 0; } - if (get_param->param != nullptr) std::memset(get_param->param, 0, sizeof(SaveDataParam)); - if (get_param->icon != nullptr) get_param->icon->data_size = 0; return SAVE_DATA_OK; } int APS5_VABI sceSaveDataInitialize3(const void* init) { (void)init; - // NotImplemented_nid_no_patch(__func__); if (g_initialized) { return SAVE_DATA_ERROR_ALREADY_INITIALIZED; } @@ -332,59 +409,108 @@ int APS5_VABI sceSaveDataSetParam(const SaveDataMountPoint* mount_point, uint32_ int APS5_VABI sceSaveDataSetSaveDataMemory2(const SaveDataMemorySet2* set_param) { if (set_param == nullptr) { - throw std::runtime_error("sceSaveDataSetSaveDataMemory2: null argument"); + return SAVE_DATA_ERROR_PARAMETER; } - std::lock_guard lock(g_state_mutex); - const auto it = g_memories.find(set_param->slot_id); - if (it == g_memories.end()) { - return SAVE_DATA_ERROR_NOT_FOUND; + if (!g_initialized) { + return SAVE_DATA_ERROR_NOT_INITIALIZED; } - auto& memory = it->second; - if (set_param->data != nullptr) { - const auto count = set_param->data_num == 0 ? 1u : set_param->data_num; - for (std::uint32_t i = 0; i < count; ++i) { - const auto& data = set_param->data[i]; - if (data.buf == nullptr) continue; - if (data.offset > memory.size() || data.buf_size > memory.size() - data.offset) { - return SAVE_DATA_ERROR_PARAMETER; - } - std::memcpy(memory.data() + data.offset, data.buf, data.buf_size); + std::lock_guard lk(g_mem_mutex); + const std::string path = mem_path(set_param->user_id, set_param->slot_id, "bin"); + std::size_t size = 0; + if (!file_size_of(path, &size)) { + return SAVE_DATA_ERROR_MEMORY_NOT_READY; + } + // Validate every range first so a bad entry never leaves a partial write. + const std::uint32_t n = set_param->data != nullptr ? (set_param->data_num != 0 ? set_param->data_num : 1u) : 0u; + for (std::uint32_t i = 0; i < n; i++) { + const SaveDataMemoryData& d = set_param->data[i]; + if (d.buf_size == 0) { + continue; + } + if (d.buf == nullptr || d.offset > size || d.buf_size > size - d.offset) { + return SAVE_DATA_ERROR_PARAMETER; } } - store_memory(set_param->slot_id, memory); + if (n != 0) { + std::fstream f(path, std::ios::binary | std::ios::in | std::ios::out); + if (!f) { + return SAVE_DATA_ERROR_INTERNAL; + } + for (std::uint32_t i = 0; i < n; i++) { + const SaveDataMemoryData& d = set_param->data[i]; + if (d.buf_size == 0) { + continue; + } + f.seekp(static_cast(d.offset)); + f.write(static_cast(d.buf), static_cast(d.buf_size)); + } + f.flush(); + if (!f) { + return SAVE_DATA_ERROR_INTERNAL; + } + } + if (set_param->param != nullptr) { + std::vector pd(sizeof(SaveDataParam)); + std::memcpy(pd.data(), set_param->param, sizeof(SaveDataParam)); + write_file_replace(mem_path(set_param->user_id, set_param->slot_id, "param"), pd); + } return SAVE_DATA_OK; } int APS5_VABI sceSaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, SaveDataMemorySetupResult* result) { if (setup_param == nullptr) { - throw std::runtime_error("sceSaveDataSetupSaveDataMemory2: null argument"); + return SAVE_DATA_ERROR_PARAMETER; } - std::lock_guard lock(g_state_mutex); - auto& memory = g_memories[setup_param->slot_id]; + if (!g_initialized) { + return SAVE_DATA_ERROR_NOT_INITIALIZED; + } + if (setup_param->memory_size == 0 || setup_param->memory_size > MEM_MAX_SIZE) { + return SAVE_DATA_ERROR_PARAMETER; + } + std::lock_guard lk(g_mem_mutex); + const std::string path = mem_path(setup_param->user_id, setup_param->slot_id, "bin"); std::size_t existed = 0; - if (memory.empty()) { - std::ifstream file(memory_path(setup_param->slot_id), std::ios::binary); - if (file) { - memory.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); - existed = memory.size(); + const bool have = file_size_of(path, &existed); + if (!have) { + existed = 0; + } + // First run: create a zero-filled blob and report existed size 0 so the title treats it as a new save. + if (!have || existed < setup_param->memory_size) { + std::error_code ec; + std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec); + std::vector data; + if (have) { + read_file_all(path, data); + } + data.resize(setup_param->memory_size, 0); + if (!write_file_replace(path, data)) { + return SAVE_DATA_ERROR_INTERNAL; + } + if (setup_param->init_param != nullptr && (setup_param->option & 1u) != 0) { + std::vector pd(sizeof(SaveDataParam)); + std::memcpy(pd.data(), setup_param->init_param, sizeof(SaveDataParam)); + write_file_replace(mem_path(setup_param->user_id, setup_param->slot_id, "param"), pd); } - } else { - existed = memory.size(); } - if (memory.size() < setup_param->memory_size) { - memory.resize(setup_param->memory_size, 0); - } - if (existed == 0) store_memory(setup_param->slot_id, memory); if (result != nullptr) { std::memset(result, 0, sizeof(*result)); - result->existed_memory_size = existed; + result->existed_memory_size = have ? existed : 0; } return SAVE_DATA_OK; } int APS5_VABI sceSaveDataSyncSaveDataMemory(const void* sync_param) { - (void)sync_param; - // Every Set already reached the file; nothing is buffered. + if (sync_param == nullptr) { + return SAVE_DATA_ERROR_PARAMETER; + } + // Every Set already writes straight through to disk; sync only has to confirm the memory exists. + const std::int32_t user_id = *static_cast(sync_param); + const std::uint32_t slot_id = reinterpret_cast(sync_param)[1]; + std::lock_guard lk(g_mem_mutex); + std::size_t size = 0; + if (!file_size_of(mem_path(user_id, slot_id, "bin"), &size)) { + return SAVE_DATA_ERROR_MEMORY_NOT_READY; + } return SAVE_DATA_OK; } @@ -401,9 +527,11 @@ int APS5_VABI sceSaveDataTerminate(void) { int APS5_VABI sceSaveDataTransferringMount(const SaveDataTransferringMount* mount, SaveDataMountResult* mount_result) { (void)mount; - (void)mount_result; - NotImplemented_nid_no_patch(__func__); - return 0; + if (mount_result != nullptr) { + std::memset(mount_result, 0, sizeof(*mount_result)); + } + // No PS4-to-PS5 transfer data exists on this console. + return SAVE_DATA_ERROR_NOT_FOUND; } int APS5_VABI sceSaveDataUmount2(uint32_t mode, const SaveDataMountPoint* mount_point) { diff --git a/core/libs/prx/libSceSaveData.native/SaveData.hpp b/core/libs/prx/libSceSaveData.native/SaveData.hpp index dd69aca6c..95cf838c1 100644 --- a/core/libs/prx/libSceSaveData.native/SaveData.hpp +++ b/core/libs/prx/libSceSaveData.native/SaveData.hpp @@ -6,15 +6,18 @@ #include constexpr int SAVE_DATA_OK = 0; +// Values follow the SDK error table (0x809F0000 + code): 2 OUT_OF_MEMORY, 3 BUSY, 4 NOT_MOUNTED, 7 EXISTS, 8 NOT_FOUND, 0xB INTERNAL, 0xC MOUNT_FULL. constexpr int SAVE_DATA_ERROR_PARAMETER = -2137063424; constexpr int SAVE_DATA_ERROR_NOT_INITIALIZED = -2137063423; -constexpr int SAVE_DATA_ERROR_ALREADY_INITIALIZED = -2137063422; -constexpr int SAVE_DATA_ERROR_OUT_OF_MEMORY = -2137063421; -constexpr int SAVE_DATA_ERROR_BUSY = -2137063420; -constexpr int SAVE_DATA_ERROR_NOT_MOUNTED = -2137063419; -constexpr int SAVE_DATA_ERROR_MOUNT_FULL = -2137063418; -constexpr int SAVE_DATA_ERROR_EXISTS = -2137063414; -constexpr int SAVE_DATA_ERROR_NOT_FOUND = -2137063413; +constexpr int SAVE_DATA_ERROR_ALREADY_INITIALIZED = -2137063419; // 0x809F0005 +constexpr int SAVE_DATA_ERROR_OUT_OF_MEMORY = -2137063422; +constexpr int SAVE_DATA_ERROR_BUSY = -2137063421; +constexpr int SAVE_DATA_ERROR_NOT_MOUNTED = -2137063420; +constexpr int SAVE_DATA_ERROR_MOUNT_FULL = -2137063412; +constexpr int SAVE_DATA_ERROR_EXISTS = -2137063417; +constexpr int SAVE_DATA_ERROR_NOT_FOUND = -2137063416; +constexpr int SAVE_DATA_ERROR_INTERNAL = -2137063413; // 0x809F000B +constexpr int SAVE_DATA_ERROR_MEMORY_NOT_READY = -2137063406; // 0x809F0012 constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_RDONLY = 1; constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_RDWR = 2; diff --git a/core/libs/prx/libSceSaveData/Export.cpp b/core/libs/prx/libSceSaveData/Export.cpp index 0bc709e26..0d44da8d1 100644 --- a/core/libs/prx/libSceSaveData/Export.cpp +++ b/core/libs/prx/libSceSaveData/Export.cpp @@ -1,53 +1,94 @@ +#include #include +#include #include #include "SceTypes.hpp" +#include "prx/libSceSaveData/SaveDataCore.hpp" #include "prx/libc/include/General.hpp" +// Savedata imports bind by NID either to libSceSaveData.native or (via the relinker alias table, +// by name) to this library. Every entry point used to return SoftFailNp (0x800242e2), which is the +// NP-family "online service not available" code - wrong dialect for a purely local save call and +// fatal-looking for callers. All entry points now delegate to SaveDataCore.hpp: a local, +// file-backed implementation that needs no PSN account and tolerates any userId +// (0 / 65535 / 268435456), mirroring the native library. + +namespace { +// Logs the first three calls of each entry point, then stays silent, keeping guest logs readable. +#define SAVEDATA_TRACE(...) \ + do { \ + static std::atomic traceCount{0}; \ + if (traceCount.fetch_add(1, std::memory_order_relaxed) < 3) { \ + std::fprintf(stderr, "[SAVEDATA:core] " __VA_ARGS__); \ + std::fputc('\n', stderr); \ + std::fflush(stderr); \ + } \ + } while (0) +} // namespace + extern "C" { +int APS5_VABI sceSaveDataInitialize3(const void* init) { + const int rc = savedata::initialize_internal(init); + SAVEDATA_TRACE("sceSaveDataInitialize3 -> 0x%08x", static_cast(rc)); + return rc; +} +int APS5_VABI sceSaveDataTerminate(void) { + const int rc = savedata::terminate_internal(); + SAVEDATA_TRACE("sceSaveDataTerminate -> 0x%08x", static_cast(rc)); + return rc; +} int APS5_VABI sceSaveDataBackup(const SaveDataBackup* backup) { - (void)backup; - NotImplemented_nid_no_patch(__func__); - return 0; + const int rc = savedata::backup_internal(backup, nullptr); + SAVEDATA_TRACE("sceSaveDataBackup -> 0x%08x", static_cast(rc)); + return rc; } - int APS5_VABI sceSaveDataGetEventResult(const void* event_param, SaveDataEvent* event) { - (void)event_param; - (void)event; - NotImplemented_nid_no_patch(__func__); - return 0; + const int rc = savedata::get_event_result_internal(event_param, event); + SAVEDATA_TRACE("sceSaveDataGetEventResult -> 0x%08x", static_cast(rc)); + return rc; } - int APS5_VABI sceSaveDataGetSaveDataMemory2(SaveDataMemoryGet2* get_param) { - (void)get_param; - NotImplemented_nid_no_patch(__func__); - return 0; + if (get_param == nullptr) { + return savedata::SD_ERROR_PARAMETER; + } + const int rc = savedata::get_memory_internal(get_param); + SAVEDATA_TRACE("get2 user=%d slot=%u -> 0x%08x", static_cast(get_param->user_id), + static_cast(get_param->slot_id), static_cast(rc)); + return rc; } - int APS5_VABI sceSaveDataSetSaveDataMemory2(const SaveDataMemorySet2* set_param) { - (void)set_param; - NotImplemented_nid_no_patch(__func__); - return 0; + if (set_param == nullptr) { + return savedata::SD_ERROR_PARAMETER; + } + const int rc = savedata::set_memory_internal(set_param); + SAVEDATA_TRACE("set2 user=%d slot=%u num=%u -> 0x%08x", + static_cast(set_param->user_id), static_cast(set_param->slot_id), + static_cast(set_param->data_num), static_cast(rc)); + return rc; } - -int APS5_VABI sceSaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, SaveDataMemorySetupResult* result) { - (void)setup_param; - (void)result; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, + SaveDataMemorySetupResult* result) { + const int rc = savedata::setup_memory_internal(setup_param, result, nullptr); + SAVEDATA_TRACE("setup2 -> 0x%08x existed=0x%zx", static_cast(rc), + result != nullptr ? result->existed_memory_size : static_cast(0)); + return rc; } - int APS5_VABI sceSaveDataSyncSaveDataMemory(const void* sync_param) { - (void)sync_param; - NotImplemented_nid_no_patch(__func__); - return 0; + if (sync_param == nullptr) { + return savedata::SD_ERROR_PARAMETER; + } + const auto* sync = static_cast(sync_param); + const int rc = savedata::sync_memory_internal(sync->user_id, sync->slot_id, sync->option, nullptr, true); + SAVEDATA_TRACE("sync user=%d slot=%u option=%u -> 0x%08x", static_cast(sync->user_id), + static_cast(sync->slot_id), static_cast(sync->option), + static_cast(rc)); + return rc; } - -int APS5_VABI sceSaveDataTransferringMount(const SaveDataTransferringMount* mount, SaveDataMountResult* mount_result) { - (void)mount; - (void)mount_result; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSaveDataTransferringMount(const SaveDataTransferringMount* mount, + SaveDataMountResult* mount_result) { + const int rc = savedata::transferring_mount_internal(mount, mount_result); + SAVEDATA_TRACE("sceSaveDataTransferringMount -> 0x%08x", static_cast(rc)); + return rc; } - } diff --git a/core/libs/prx/libSceSaveData/SaveDataCore.hpp b/core/libs/prx/libSceSaveData/SaveDataCore.hpp new file mode 100644 index 000000000..166176bfe --- /dev/null +++ b/core/libs/prx/libSceSaveData/SaveDataCore.hpp @@ -0,0 +1,1354 @@ +#ifndef CORE_LIBS_PRX_LIBSCESAVEDATA_SAVEDATACORE_HPP +#define CORE_LIBS_PRX_LIBSCESAVEDATA_SAVEDATACORE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace savedata { + +extern "C" { +int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, std::uint32_t attr, + std::uint64_t init_pattern, const void* param); +int APS5_VABI sceKernelDeleteEventFlag(KernelEventFlag ef); +int APS5_VABI sceKernelSetEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern); +int APS5_VABI sceKernelCancelEventFlag(KernelEventFlag ef, std::uint64_t set_pattern, int* num_wait_threads); +} + +namespace { + +constexpr std::uint32_t SD_FACILITY = 0x809F0000u; +constexpr int sd_error(std::uint32_t code) { return static_cast(SD_FACILITY + code); } + +constexpr int SD_OK = 0; +constexpr int SD_ERROR_PARAMETER = sd_error(0x00); +constexpr int SD_ERROR_NOT_INITIALIZED = sd_error(0x01); +constexpr int SD_ERROR_OUT_OF_MEMORY = sd_error(0x02); +constexpr int SD_ERROR_BUSY = sd_error(0x03); +constexpr int SD_ERROR_NOT_MOUNTED = sd_error(0x04); +constexpr int SD_ERROR_NO_PERMISSION = sd_error(0x05); +constexpr int SD_ERROR_FINGERPRINT_MISMATCH = sd_error(0x06); +constexpr int SD_ERROR_EXISTS = sd_error(0x07); +constexpr int SD_ERROR_NOT_FOUND = sd_error(0x08); +constexpr int SD_ERROR_NO_SPACE_FS = sd_error(0x0A); +constexpr int SD_ERROR_INTERNAL = sd_error(0x0B); +constexpr int SD_ERROR_MOUNT_FULL = sd_error(0x0C); +constexpr int SD_ERROR_BAD_MOUNTED = sd_error(0x0D); +constexpr int SD_ERROR_FILE_NOT_FOUND = sd_error(0x0E); +constexpr int SD_ERROR_BROKEN = sd_error(0x0F); +constexpr int SD_ERROR_INVALID_LOGIN_USER = sd_error(0x11); +constexpr int SD_ERROR_MEMORY_NOT_READY = sd_error(0x12); +constexpr int SD_ERROR_BACKUP_BUSY = sd_error(0x13); +constexpr int SD_ERROR_NOT_REGIST_CALLBACK = sd_error(0x15); +constexpr int SD_ERROR_BUSY_FOR_SAVING = sd_error(0x16); +constexpr int SD_ERROR_LIMITATION_OVER = sd_error(0x17); +constexpr int SD_ERROR_EVENT_BUSY = sd_error(0x18); + +constexpr std::uint32_t SD_MOUNT_RDONLY = 1u; +constexpr std::uint32_t SD_MOUNT_RDWR = 2u; +constexpr std::uint32_t SD_MOUNT_CREATE = 4u; +constexpr std::uint32_t SD_MOUNT_COPY_ICON = 16u; +constexpr std::uint32_t SD_MOUNT_CREATE2 = 32u; + +constexpr std::uint32_t SD_UMOUNT_BACKUP_ASYNC = 1u << 16u; +constexpr std::uint32_t SD_COMMIT_BACKUP_ASYNC = 1u; + +constexpr std::uint32_t SD_EVENT_UMOUNT_BACKUP_END = 1u; +constexpr std::uint32_t SD_EVENT_BACKUP_END = 2u; +constexpr std::uint32_t SD_EVENT_MEMORY_SYNC_END = 3u; +constexpr std::uint32_t SD_EVENT_COMMIT_BACKUP_END = 4u; + +constexpr std::uint64_t SD_BLOCKS_MIN = 1; +constexpr std::uint64_t SD_BLOCKS_MAX = 32768; + +constexpr std::uint32_t SD_MEMORY_SLOTS = 4; +constexpr std::uint32_t SD_MEMORY_MAX_MOUNTED = 4; +constexpr std::size_t SD_MEMORY_MAX_SIZE = 32u * 1024u * 1024u; +constexpr std::uint32_t SD_MEMORY_SET_PARAM = 1u; +constexpr std::uint32_t SD_MEMORY_DOUBLE_BUFFER = 2u; +constexpr std::uint32_t SD_MEMORY_MAX_DATA_NUM = 5; + +constexpr std::uint32_t SD_PARAM_WHOLE = 0; +constexpr std::uint32_t SD_PARAM_TITLE = 1; +constexpr std::uint32_t SD_PARAM_SUB_TITLE = 2; +constexpr std::uint32_t SD_PARAM_DETAIL = 3; +constexpr std::uint32_t SD_PARAM_USER_PARAM = 4; +constexpr std::uint32_t SD_PARAM_MTIME = 5; + +constexpr std::uint32_t SD_APP_STATUS_NONE = 0; +constexpr std::uint32_t SD_APP_STATUS_DELETED = 1; +constexpr std::uint32_t SD_APP_STATUS_LOCAL = 2; +constexpr std::uint32_t SD_APP_STATUS_ONLINE = 4; +constexpr std::uint32_t SD_APP_STATUS_ANY = 0x7FFFFFFFu; + +constexpr char SD_ROOT_GUEST[] = "/_sd"; +constexpr char SD_SUBDIR[] = "sce_sys"; +constexpr char SD_MEMORY_PREFIX[] = "sce_sdmemory"; +constexpr std::size_t SD_MOUNT_PREFIX_LEN = sizeof(SD_ROOT_GUEST); +constexpr std::size_t SD_MOUNT_POINT_CAPACITY = sizeof(SaveDataMountPoint) - SD_MOUNT_PREFIX_LEN - 1; + +struct MemoryData { + void* buf; + std::size_t buf_size; + std::int64_t offset; + std::uint8_t reserved[40]; +}; +static_assert(sizeof(MemoryData) == 64, "SceSaveDataMemoryData is 64 bytes"); + +struct MemorySync { + std::int32_t user_id; + std::uint32_t slot_id; + std::uint32_t option; + std::uint8_t reserved[28]; +}; +static_assert(sizeof(MemorySync) == 40, "SceSaveDataMemorySync is 40 bytes"); + +struct EventParam { + std::size_t size; + std::uint8_t reserved[24]; +}; + +struct MountSlot { + bool used = false; + std::string mount_point; + std::string dir_name; + std::filesystem::path directory; +}; + +struct MemorySlot { + std::vector data; + SaveDataParam param {}; + std::uint32_t option = 0; + bool dirty = false; +}; + +struct PendingEvent { + SaveDataEvent record {}; + KernelEventFlag flag = nullptr; +}; + +struct State { + std::mutex mutex; + std::uint32_t instances = 0; + std::array slots {}; + std::deque events; + std::unordered_map memory; + std::map aliases; + std::int32_t next_transaction_resource = 1; + std::set live_transaction_resources; + void* event_callback = nullptr; + void* event_callback_userdata = nullptr; + bool aliases_loaded = false; +}; + +State& state() { + static State instance; + return instance; +} + +bool is_ready() { + return state().instances != 0; +} + +bool valid_dir_name(const char* name) { + if (name == nullptr || name[0] == '\0') { + return false; + } + std::size_t length = 0; + while (name[length] != '\0') { + length++; + if (length >= sizeof(SceSaveDataDirName::data)) { + return false; + } + } + if (std::strcmp(name, ".") == 0 || std::strcmp(name, "..") == 0) { + return false; + } + for (const char* p = name; *p != '\0'; p++) { + const bool ok = std::isalnum(static_cast(*p)) != 0 || *p == '_' || *p == '-' || + *p == '.' || *p == '@'; + if (!ok) { + return false; + } + } + return true; +} + +bool bounded_string_ok(const char* data, std::size_t capacity) { + return std::memchr(data, '\0', capacity) != nullptr; +} + +const std::filesystem::path& save_root() { + static const std::filesystem::path root = ResolvePath_nid_no_patch(SD_ROOT_GUEST); + return root; +} + +bool ensure_save_root(std::error_code& error) { + std::filesystem::create_directories(save_root(), error); + if (error) { + error.clear(); + } + return std::filesystem::is_directory(save_root(), error); +} + +void load_aliases_locked(State& st) { + if (st.aliases_loaded) { + return; + } + st.aliases_loaded = true; + std::ifstream file(save_root() / "_aliases", std::ios::binary); + std::string line; + while (std::getline(file, line)) { + const std::size_t sep = line.find('\t'); + if (sep == std::string::npos || sep == 0) { + continue; + } + st.aliases[line.substr(0, sep)] = line.substr(sep + 1); + } +} + +void store_aliases_locked(State& st) { + std::error_code error; + ensure_save_root(error); + std::ofstream file(save_root() / "_aliases", std::ios::binary | std::ios::trunc); + for (const auto& [alias, dir_name] : st.aliases) { + file << alias << '\t' << dir_name << '\n'; + } +} + +std::string save_alias_locked(State& st, const std::string& dir_name) { + if (dir_name.size() <= SD_MOUNT_POINT_CAPACITY) { + return dir_name; + } + load_aliases_locked(st); + const std::string base = dir_name.substr(0, 6); + std::uint32_t hash = 2166136261u; + for (const char c : dir_name) { + hash = (hash ^ static_cast(c)) * 16777619u; + } + for (std::uint32_t attempt = 0; attempt < 4096u; attempt++) { + char tail[8]; + std::snprintf(tail, sizeof(tail), "~%03X", static_cast((hash + attempt) & 0xFFFu)); + const std::string candidate = base + tail; + const auto found = st.aliases.find(candidate); + if (found == st.aliases.end()) { + st.aliases[candidate] = dir_name; + store_aliases_locked(st); + return candidate; + } + if (found->second == dir_name) { + return candidate; + } + } + return base; +} + +std::string real_dir_name_locked(State& st, const std::string& alias) { + load_aliases_locked(st); + const auto found = st.aliases.find(alias); + return found != st.aliases.end() ? found->second : alias; +} + +std::string mount_point_for(const std::string& alias) { + return std::string(SD_ROOT_GUEST) + "/" + alias; +} + +std::filesystem::path save_directory(const std::string& alias) { + return save_root() / alias; +} + +std::filesystem::path save_metadata_directory(const std::filesystem::path& directory) { + return directory / SD_SUBDIR; +} + +std::filesystem::path save_param_path(const std::filesystem::path& directory) { + return save_metadata_directory(directory) / "param.bin"; +} + +std::filesystem::path save_blocks_path(const std::filesystem::path& directory) { + return save_metadata_directory(directory) / "blocks.bin"; +} + +bool write_blob(const std::filesystem::path& path, const void* data, std::size_t size) { + std::error_code error; + std::filesystem::create_directories(path.parent_path(), error); + const std::filesystem::path temporary = path.native() + L".tmp"; + { + std::ofstream file(temporary, std::ios::binary | std::ios::trunc); + if (!file) { + return false; + } + if (size != 0) { + file.write(static_cast(data), static_cast(size)); + } + file.flush(); + if (!file) { + return false; + } + } + error.clear(); + std::filesystem::rename(temporary, path, error); + if (error) { + std::error_code ignored; + std::filesystem::remove(path, ignored); + error.clear(); + std::filesystem::rename(temporary, path, error); + } + std::error_code leftovers; + std::filesystem::remove(temporary, leftovers); + return !error; +} + +bool read_blob(const std::filesystem::path& path, void* data, std::size_t size) { + std::ifstream file(path, std::ios::binary); + if (!file) { + return false; + } + file.read(static_cast(data), static_cast(size)); + return static_cast(file.gcount()) == size; +} + +int write_u64(const std::filesystem::path& path, std::uint64_t value) { + return write_blob(path, &value, sizeof(value)) ? SD_OK : SD_ERROR_INTERNAL; +} + +int read_u64(const std::filesystem::path& path, std::uint64_t* value) { + std::error_code error; + if (!std::filesystem::is_regular_file(path, error) || error) { + return SD_ERROR_BROKEN; + } + if (std::filesystem::file_size(path, error) != sizeof(std::uint64_t) || error) { + return SD_ERROR_BROKEN; + } + return read_blob(path, value, sizeof(*value)) ? SD_OK : SD_ERROR_BROKEN; +} + +int load_save_param(const std::filesystem::path& directory, SaveDataParam* param) { + std::memset(param, 0, sizeof(*param)); + const int status = read_blob(save_param_path(directory), param, sizeof(*param)) ? SD_OK : SD_ERROR_NOT_FOUND; + if (status == SD_OK) { + std::int64_t newest = 0; + std::error_code error; + for (std::filesystem::recursive_directory_iterator it(directory, + std::filesystem::directory_options::skip_permission_denied, error), end; + it != end; it.increment(error)) { + if (error) { + error.clear(); + continue; + } + std::error_code file_error; + if (it->is_regular_file(file_error) && !file_error) { + const auto written = std::filesystem::last_write_time(it->path(), file_error); + if (!file_error) { + const auto seconds = std::chrono::duration_cast( + std::filesystem::file_time_type::clock::now() - written).count(); + const auto now = static_cast(std::time(nullptr)); + newest = std::max(newest, now - static_cast(seconds)); + } + } + } + param->mtime = newest; + } + return status; +} + +bool save_is_mounted_locked(const State& st, const std::filesystem::path& directory) { + for (const auto& slot : st.slots) { + if (slot.used && slot.directory == directory) { + return true; + } + } + return false; +} + +int slot_of_mount_point_locked(const State& st, const char* mount_point) { + for (std::size_t i = 0; i < st.slots.size(); i++) { + if (st.slots[i].used && st.slots[i].mount_point == mount_point) { + return static_cast(i); + } + } + return -1; +} + +int free_slot_locked(const State& st) { + for (std::size_t i = 0; i < st.slots.size(); i++) { + if (!st.slots[i].used) { + return static_cast(i); + } + } + return -1; +} + +void queue_event_locked(State& st, std::uint32_t type, std::int32_t user_id, + const SceSaveDataTitleId* title_id, const SceSaveDataDirName* dir_name, + int error_code, KernelEventFlag flag) { + PendingEvent pending; + pending.record = {}; + pending.record.type = type; + pending.record.error_code = error_code; + pending.record.user_id = user_id; + if (title_id != nullptr) { + std::memcpy(&pending.record.title_id, title_id, sizeof(pending.record.title_id)); + } + if (dir_name != nullptr) { + std::memcpy(&pending.record.dir_name, dir_name, sizeof(pending.record.dir_name)); + } + pending.flag = flag; + st.events.push_back(pending); + while (st.events.size() > 64) { + if (st.events.front().flag != nullptr) { + sceKernelDeleteEventFlag(st.events.front().flag); + } + st.events.pop_front(); + } +} + +KernelEventFlag create_signalled_flag(const char* name) { + KernelEventFlag flag = nullptr; + if (sceKernelCreateEventFlag(&flag, name, 0x20u, 0, nullptr) != 0) { + return nullptr; + } + sceKernelSetEventFlag(flag, 1u); + return flag; +} + +int initialize_internal(const void* init) { + State& st = state(); + std::lock_guard lock(st.mutex); + st.instances++; + return SD_OK; +} + +int terminate_internal() { + State& st = state(); + std::lock_guard lock(st.mutex); + if (st.instances == 0) { + return SD_ERROR_NOT_INITIALIZED; + } + if (std::any_of(st.slots.begin(), st.slots.end(), + [](const MountSlot& slot) { return slot.used; }) || !st.memory.empty()) { + return SD_ERROR_BUSY; + } + st.instances--; + if (st.instances == 0) { + for (auto& pending : st.events) { + if (pending.flag != nullptr) { + sceKernelDeleteEventFlag(pending.flag); + } + } + st.events.clear(); + st.memory.clear(); + st.live_transaction_resources.clear(); + } + return SD_OK; +} + +int mount_internal(const SceSaveDataDirName* dir_name, std::uint32_t mount_mode, + std::uint64_t blocks, std::int32_t user_id, SaveDataMountResult* mount_result, + const char* api) { + if (mount_result == nullptr) { + return SD_ERROR_PARAMETER; + } + std::memset(mount_result, 0, sizeof(*mount_result)); + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (dir_name == nullptr || !bounded_string_ok(dir_name->data, sizeof(dir_name->data)) || + !valid_dir_name(dir_name->data)) { + return SD_ERROR_PARAMETER; + } + if (user_id < 0) { + return SD_ERROR_INVALID_LOGIN_USER; + } + const bool create = (mount_mode & SD_MOUNT_CREATE) != 0; + const bool create_or_open = (mount_mode & SD_MOUNT_CREATE2) != 0; + const bool open = !create && !create_or_open && (mount_mode & (SD_MOUNT_RDONLY | SD_MOUNT_RDWR)) != 0; + if (!create && !create_or_open && !open) { + return SD_ERROR_PARAMETER; + } + + State& st = state(); + std::lock_guard lock(st.mutex); + const std::string requested = dir_name->data; + const std::string alias = save_alias_locked(st, requested); + const std::string mount_point = mount_point_for(alias); + if (mount_point.size() + 1 > sizeof(mount_result->mount_point)) { + return SD_ERROR_PARAMETER; + } + const std::filesystem::path directory = save_directory(alias); + for (const auto& slot : st.slots) { + if (slot.used && slot.directory == directory) { + return SD_ERROR_BUSY; + } + } + const int free_index = free_slot_locked(st); + if (free_index == -1) { + return SD_ERROR_MOUNT_FULL; + } + + std::error_code error; + const bool exists = std::filesystem::is_directory(directory, error) && !error; + if (open && !exists) { + return SD_ERROR_NOT_FOUND; + } + if (create && exists) { + return SD_ERROR_EXISTS; + } + bool created = false; + if ((create || create_or_open) && !exists) { + if (blocks < SD_BLOCKS_MIN || blocks > SD_BLOCKS_MAX) { + return SD_ERROR_PARAMETER; + } + std::error_code root_error; + if (!ensure_save_root(root_error)) { + return SD_ERROR_INTERNAL; + } + if (!std::filesystem::create_directories(directory, error) || error) { + return SD_ERROR_INTERNAL; + } + created = true; + if (write_u64(save_blocks_path(directory), blocks) != SD_OK) { + std::error_code cleanup; + std::filesystem::remove_all(directory, cleanup); + return SD_ERROR_INTERNAL; + } + SaveDataParam initial {}; + std::strncpy(initial.title, "Saved Data", sizeof(initial.title) - 1); + if (!write_blob(save_param_path(directory), &initial, sizeof(initial))) { + std::error_code cleanup; + std::filesystem::remove_all(directory, cleanup); + return SD_ERROR_INTERNAL; + } + } + if (exists && !created) { + std::uint64_t recorded = 0; + if (read_u64(save_blocks_path(directory), &recorded) != SD_OK && blocks != 0) { + if (blocks < SD_BLOCKS_MIN || blocks > SD_BLOCKS_MAX) { + return SD_ERROR_PARAMETER; + } + write_u64(save_blocks_path(directory), blocks); + } + } + + MountSlot& slot = st.slots[static_cast(free_index)]; + slot.used = true; + slot.mount_point = mount_point; + slot.dir_name = requested; + slot.directory = directory; + std::memcpy(mount_result->mount_point.data, mount_point.c_str(), mount_point.size() + 1); + mount_result->required_blocks = 0; + mount_result->mount_status = created ? 1u : 0u; + return SD_OK; +} + +int umount_internal(std::uint32_t mode, const SaveDataMountPoint* mount_point, const char* api) { + State& st = state(); + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (mount_point == nullptr || !bounded_string_ok(mount_point->data, sizeof(mount_point->data))) { + return SD_ERROR_PARAMETER; + } + std::lock_guard lock(st.mutex); + const int index = slot_of_mount_point_locked(st, mount_point->data); + if (index == -1) { + return SD_ERROR_NOT_MOUNTED; + } + st.slots[static_cast(index)] = MountSlot {}; + if ((mode & SD_UMOUNT_BACKUP_ASYNC) != 0) { + queue_event_locked(st, SD_EVENT_UMOUNT_BACKUP_END, 0, nullptr, nullptr, SD_OK, nullptr); + } + return SD_OK; +} + +int delete_internal(std::int32_t user_id, const SceSaveDataDirName* dir_name, + const SceSaveDataTitleId* title_id) { + State& st = state(); + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (dir_name == nullptr || !bounded_string_ok(dir_name->data, sizeof(dir_name->data)) || + !valid_dir_name(dir_name->data) || + (title_id != nullptr && !bounded_string_ok(title_id->data, sizeof(title_id->data)))) { + return SD_ERROR_PARAMETER; + } + if (user_id < 0) { + return SD_ERROR_INVALID_LOGIN_USER; + } + std::lock_guard lock(st.mutex); + const std::string alias = save_alias_locked(st, dir_name->data); + const std::filesystem::path directory = save_directory(alias); + if (save_is_mounted_locked(st, directory)) { + return SD_ERROR_BUSY; + } + std::error_code error; + if (!std::filesystem::exists(directory, error) || error) { + return SD_ERROR_NOT_FOUND; + } + std::filesystem::remove_all(directory, error); + if (error) { + return SD_ERROR_INTERNAL; + } + st.aliases.erase(alias); + if (alias != dir_name->data) { + store_aliases_locked(st); + } + return SD_OK; +} + +bool dir_name_matches(const char* str, const char* pattern) { + if (pattern == nullptr || pattern[0] == '\0') { + return true; + } + while (*str != '\0' && *pattern != '\0') { + if (*pattern == '%') { + for (const char* s = str;; s++) { + if (dir_name_matches(s, pattern + 1)) { + return true; + } + if (*s == '\0') { + break; + } + } + return false; + } + if (*pattern == '_') { + str++; + pattern++; + continue; + } + if (std::tolower(static_cast(*str)) != + std::tolower(static_cast(*pattern))) { + return false; + } + str++; + pattern++; + } + return *str == '\0' && *pattern == '\0'; +} + +int dir_name_search_internal(const SaveDataDirNameSearchCond* cond, + SaveDataDirNameSearchResult* result) { + State& st = state(); + if (result != nullptr) { + std::memset(result, 0, sizeof(*result)); + } + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (cond == nullptr || result == nullptr || + (result->dir_names_num != 0 && result->dir_names == nullptr) || + (cond->dir_name != nullptr && !bounded_string_ok(cond->dir_name->data, sizeof(cond->dir_name->data))) || + (cond->title_id != nullptr && !bounded_string_ok(cond->title_id->data, sizeof(cond->title_id->data)))) { + return SD_ERROR_PARAMETER; + } + if (cond->user_id < 0) { + return SD_ERROR_INVALID_LOGIN_USER; + } + std::lock_guard lock(st.mutex); + load_aliases_locked(st); + std::vector found; + std::error_code error; + if (std::filesystem::is_directory(save_root(), error) && !error) { + for (const auto& entry : std::filesystem::directory_iterator(save_root(), error)) { + if (error) { + break; + } + std::error_code kind_error; + if (!entry.is_directory(kind_error) || kind_error) { + continue; + } + const std::string alias = entry.path().filename().string(); + if (alias.empty() || alias[0] == '.' || alias[0] == '_' || + !valid_dir_name(alias.c_str())) { + continue; + } + std::error_code meta_error; + if (!std::filesystem::is_regular_file(save_param_path(entry.path()), meta_error) || meta_error) { + continue; + } + const std::string dir_name = real_dir_name_locked(st, alias); + if (cond->dir_name != nullptr && !dir_name_matches(dir_name.c_str(), cond->dir_name->data)) { + continue; + } + found.push_back(dir_name); + } + } + std::sort(found.begin(), found.end()); + if (cond->order == 1) { + std::reverse(found.begin(), found.end()); + } + result->hit_num = static_cast(found.size()); + const std::uint32_t set_count = std::min(result->dir_names_num, found.size()); + for (std::uint32_t i = 0; i < set_count; i++) { + std::snprintf(result->dir_names[i].data, sizeof(result->dir_names[i].data), "%s", + found[i].c_str()); + if (result->params != nullptr) { + load_save_param(save_directory(save_alias_locked(st, found[i])), &result->params[i]); + } + if (result->infos != nullptr) { + std::memset(&result->infos[i], 0, sizeof(result->infos[i])); + std::uint64_t blocks = 0; + if (read_u64(save_blocks_path(save_directory(save_alias_locked(st, found[i]))), &blocks) == SD_OK) { + std::memcpy(&result->infos[i], &blocks, sizeof(blocks)); + } + } + } + result->set_num = set_count; + return SD_OK; +} + +int get_param_internal(const SaveDataMountPoint* mount_point, std::uint32_t param_type, + void* param_buf, std::size_t param_buf_size, std::size_t* got_size) { + State& st = state(); + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (mount_point == nullptr || param_buf == nullptr || param_type > SD_PARAM_MTIME || + !bounded_string_ok(mount_point->data, sizeof(mount_point->data))) { + return SD_ERROR_PARAMETER; + } + std::lock_guard lock(st.mutex); + const int index = slot_of_mount_point_locked(st, mount_point->data); + if (index == -1) { + return SD_ERROR_NOT_MOUNTED; + } + SaveDataParam param {}; + const int status = load_save_param(st.slots[static_cast(index)].directory, ¶m); + if (status != SD_OK) { + return status; + } + std::size_t written = 0; + if (param_type == SD_PARAM_WHOLE) { + if (param_buf_size < sizeof(param)) { + return SD_ERROR_PARAMETER; + } + std::memcpy(param_buf, ¶m, sizeof(param)); + written = sizeof(param); + } else if (param_type == SD_PARAM_USER_PARAM || param_type == SD_PARAM_MTIME) { + const std::size_t size = param_type == SD_PARAM_USER_PARAM ? sizeof(param.user_param) + : sizeof(param.mtime); + if (param_buf_size < size) { + return SD_ERROR_PARAMETER; + } + std::memcpy(param_buf, + param_type == SD_PARAM_USER_PARAM + ? static_cast(¶m.user_param) + : static_cast(¶m.mtime), + size); + written = size; + } else { + char* field = param_type == SD_PARAM_TITLE ? param.title + : param_type == SD_PARAM_SUB_TITLE ? param.sub_title : param.detail; + const std::size_t capacity = param_type == SD_PARAM_DETAIL ? sizeof(param.detail) + : sizeof(param.title); + if (param_buf_size == 0) { + return SD_ERROR_PARAMETER; + } + const std::size_t length = std::min(std::strlen(field), param_buf_size - 1); + std::memcpy(param_buf, field, length); + static_cast(param_buf)[length] = '\0'; + written = length + 1; + } + if (got_size != nullptr) { + *got_size = written; + } + return SD_OK; +} + +int set_param_internal(const SaveDataMountPoint* mount_point, std::uint32_t param_type, + const void* param_buf, std::size_t param_buf_size) { + State& st = state(); + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (mount_point == nullptr || param_buf == nullptr || param_type > SD_PARAM_MTIME || + !bounded_string_ok(mount_point->data, sizeof(mount_point->data))) { + return SD_ERROR_PARAMETER; + } + std::lock_guard lock(st.mutex); + const int index = slot_of_mount_point_locked(st, mount_point->data); + if (index == -1) { + return SD_ERROR_NOT_MOUNTED; + } + const std::filesystem::path directory = st.slots[static_cast(index)].directory; + if (param_type == SD_PARAM_WHOLE) { + if (param_buf_size < sizeof(SaveDataParam)) { + return SD_ERROR_PARAMETER; + } + SaveDataParam param = *static_cast(param_buf); + param.mtime = static_cast(std::time(nullptr)); + return write_blob(save_param_path(directory), ¶m, sizeof(param)) ? SD_OK : SD_ERROR_INTERNAL; + } + SaveDataParam param {}; + const int status = load_save_param(directory, ¶m); + if (status != SD_OK && status != SD_ERROR_NOT_FOUND) { + return status; + } + if (param_type == SD_PARAM_USER_PARAM || param_type == SD_PARAM_MTIME) { + const std::size_t size = param_type == SD_PARAM_USER_PARAM ? sizeof(param.user_param) + : sizeof(param.mtime); + if (param_buf_size < size) { + return SD_ERROR_PARAMETER; + } + std::memcpy(param_type == SD_PARAM_USER_PARAM ? static_cast(¶m.user_param) + : static_cast(¶m.mtime), + param_buf, size); + } else { + char* field = param_type == SD_PARAM_TITLE ? param.title + : param_type == SD_PARAM_SUB_TITLE ? param.sub_title : param.detail; + const std::size_t capacity = param_type == SD_PARAM_DETAIL ? sizeof(param.detail) + : sizeof(param.title); + if (param_buf_size == 0) { + return SD_ERROR_PARAMETER; + } + const std::size_t length = std::min(param_buf_size, capacity - 1); + const char* text = static_cast(param_buf); + const char* end = static_cast(std::memchr(text, '\0', length)); + std::memset(field, 0, capacity); + std::memcpy(field, text, end != nullptr ? static_cast(end - text) : length); + } + return write_blob(save_param_path(directory), ¶m, sizeof(param)) ? SD_OK : SD_ERROR_INTERNAL; +} + +int get_mount_info_internal(const SaveDataMountPoint* mount_point, SaveDataMountInfo* info) { + State& st = state(); + if (info != nullptr) { + std::memset(info, 0, sizeof(*info)); + } + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (mount_point == nullptr || info == nullptr || + !bounded_string_ok(mount_point->data, sizeof(mount_point->data))) { + return SD_ERROR_PARAMETER; + } + std::lock_guard lock(st.mutex); + const int index = slot_of_mount_point_locked(st, mount_point->data); + if (index == -1) { + return SD_ERROR_NOT_MOUNTED; + } + std::uint64_t blocks = 0; + const int status = read_u64(save_blocks_path(st.slots[static_cast(index)].directory), + &blocks); + if (status != SD_OK) { + return status; + } + info->blocks = blocks; + info->free_blocks = blocks; + return SD_OK; +} + +int is_mounted_internal(const SaveDataMountPoint* mount_point, std::uint32_t* is_mounted) { + if (mount_point == nullptr || is_mounted == nullptr || + !bounded_string_ok(mount_point->data, sizeof(mount_point->data))) { + return SD_ERROR_PARAMETER; + } + State& st = state(); + std::lock_guard lock(st.mutex); + *is_mounted = slot_of_mount_point_locked(st, mount_point->data) != -1 ? 1u : 0u; + return SD_OK; +} + +std::int64_t count_mounted_internal() { + State& st = state(); + std::lock_guard lock(st.mutex); + std::int64_t count = 0; + for (const auto& slot : st.slots) { + if (slot.used) { + count++; + } + } + return count; +} + +std::string memory_key(std::int32_t user_id, std::uint32_t slot_id) { + char key[32]; + std::snprintf(key, sizeof(key), "%d:%u", static_cast(user_id), static_cast(slot_id)); + return key; +} + +std::filesystem::path memory_directory(std::int32_t user_id, std::uint32_t slot_id) { + char name[48]; + std::snprintf(name, sizeof(name), "%s%s", SD_MEMORY_PREFIX, + slot_id == 0 ? "" : std::to_string(slot_id).c_str()); + return save_root() / name / std::to_string(static_cast(user_id)); +} + +bool valid_memory_range(const MemoryData& data, std::size_t size) { + return data.buf != nullptr && data.offset >= 0 && + static_cast(data.offset) <= size && + data.buf_size <= size - static_cast(data.offset); +} + +int setup_memory_internal(const SaveDataMemorySetup2* setup, SaveDataMemorySetupResult* result, + KernelEventFlag flag) { + State& st = state(); + if (setup == nullptr || setup->slot_id >= SD_MEMORY_SLOTS || setup->memory_size == 0 || + setup->memory_size > SD_MEMORY_MAX_SIZE || (setup->option & ~(SD_MEMORY_SET_PARAM | SD_MEMORY_DOUBLE_BUFFER)) != 0 || + (setup->init_param != nullptr && setup->option != 0 && (setup->option & SD_MEMORY_SET_PARAM) == 0)) { + return SD_ERROR_PARAMETER; + } + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (setup->user_id < 0) { + return SD_ERROR_INVALID_LOGIN_USER; + } + std::lock_guard lock(st.mutex); + const std::string key = memory_key(setup->user_id, setup->slot_id); + if (st.memory.count(key) != 0) { + return SD_ERROR_BUSY; + } + if (st.memory.size() >= SD_MEMORY_MAX_MOUNTED) { + return SD_ERROR_LIMITATION_OVER; + } + if (save_is_mounted_locked(st, memory_directory(setup->user_id, setup->slot_id))) { + return SD_ERROR_BUSY; + } + MemorySlot slot; + slot.option = setup->option; + slot.data.resize(setup->memory_size, 0); + std::strncpy(slot.param.title, "Saved Data", sizeof(slot.param.title) - 1); + std::size_t existed = 0; + std::error_code error; + const std::filesystem::path blob = memory_directory(setup->user_id, setup->slot_id) / "memory.dat"; + if (std::filesystem::is_regular_file(blob, error) && !error) { + const std::size_t stored = static_cast(std::filesystem::file_size(blob, error)); + if (error || stored > SD_MEMORY_MAX_SIZE) { + return SD_ERROR_BROKEN; + } + existed = stored; + std::vector loaded(stored, 0); + std::ifstream file(blob, std::ios::binary); + file.read(loaded.data(), static_cast(stored)); + if (!file) { + return SD_ERROR_BROKEN; + } + std::copy_n(loaded.begin(), std::min(stored, slot.data.size()), slot.data.begin()); + load_save_param(memory_directory(setup->user_id, setup->slot_id), &slot.param); + } + if (existed == 0 && setup->init_param != nullptr && (setup->option & SD_MEMORY_SET_PARAM) != 0) { + slot.param = *setup->init_param; + slot.param.mtime = 0; + } + st.memory.emplace(key, std::move(slot)); + if (result != nullptr) { + std::memset(result, 0, sizeof(*result)); + result->existed_memory_size = existed; + } + if (flag != nullptr) { + queue_event_locked(st, SD_EVENT_MEMORY_SYNC_END, setup->user_id, nullptr, nullptr, SD_OK, flag); + } + return SD_OK; +} + +int get_memory_internal(SaveDataMemoryGet2* get_param) { + State& st = state(); + if (get_param == nullptr || get_param->slot_id >= SD_MEMORY_SLOTS) { + return SD_ERROR_PARAMETER; + } + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (get_param->user_id < 0) { + return SD_ERROR_INVALID_LOGIN_USER; + } + std::lock_guard lock(st.mutex); + const auto it = st.memory.find(memory_key(get_param->user_id, get_param->slot_id)); + if (it == st.memory.end()) { + return SD_ERROR_MEMORY_NOT_READY; + } + const MemorySlot& slot = it->second; + if (get_param->data != nullptr && + !valid_memory_range(*reinterpret_cast(get_param->data), slot.data.size())) { + return SD_ERROR_PARAMETER; + } + if (get_param->param != nullptr && (slot.option & SD_MEMORY_SET_PARAM) == 0) { + return SD_ERROR_PARAMETER; + } + if (get_param->data != nullptr) { + const MemoryData& data = *reinterpret_cast(get_param->data); + std::memcpy(data.buf, slot.data.data() + data.offset, data.buf_size); + } + if (get_param->param != nullptr) { + *get_param->param = slot.param; + } + if (get_param->icon != nullptr) { + get_param->icon->data_size = 0; + } + return SD_OK; +} + +int set_memory_internal(const SaveDataMemorySet2* set_param) { + State& st = state(); + if (set_param == nullptr || set_param->slot_id >= SD_MEMORY_SLOTS || + set_param->data_num > SD_MEMORY_MAX_DATA_NUM || + (set_param->data == nullptr && set_param->data_num != 0)) { + return SD_ERROR_PARAMETER; + } + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (set_param->user_id < 0) { + return SD_ERROR_INVALID_LOGIN_USER; + } + std::lock_guard lock(st.mutex); + const auto it = st.memory.find(memory_key(set_param->user_id, set_param->slot_id)); + if (it == st.memory.end()) { + return SD_ERROR_MEMORY_NOT_READY; + } + if (set_param->param != nullptr && (it->second.option & SD_MEMORY_SET_PARAM) == 0) { + return SD_ERROR_PARAMETER; + } + const std::uint32_t count = + set_param->data != nullptr && set_param->data_num == 0 ? 1u : set_param->data_num; + const MemoryData* descriptors = reinterpret_cast(set_param->data); + for (std::uint32_t i = 0; i < count; i++) { + if (!valid_memory_range(descriptors[i], it->second.data.size())) { + return SD_ERROR_PARAMETER; + } + } + MemorySlot updated = it->second; + for (std::uint32_t i = 0; i < count; i++) { + const MemoryData& data = descriptors[i]; + std::memcpy(updated.data.data() + data.offset, data.buf, data.buf_size); + } + if (set_param->param != nullptr) { + updated.param = *set_param->param; + } + updated.param.mtime = static_cast(std::time(nullptr)); + updated.dirty = true; + it->second = std::move(updated); + return SD_OK; +} + +int sync_memory_internal(const std::int32_t user_id, const std::uint32_t slot_id, + const std::uint32_t option, KernelEventFlag flag, bool report_event) { + State& st = state(); + if (slot_id >= SD_MEMORY_SLOTS || option > 1) { + return SD_ERROR_PARAMETER; + } + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (user_id < 0) { + return SD_ERROR_INVALID_LOGIN_USER; + } + std::lock_guard lock(st.mutex); + const auto it = st.memory.find(memory_key(user_id, slot_id)); + if (it == st.memory.end()) { + return SD_ERROR_MEMORY_NOT_READY; + } + const std::filesystem::path directory = memory_directory(user_id, slot_id); + int status = SD_OK; + if (!write_blob(directory / "memory.dat", it->second.data.data(), it->second.data.size())) { + status = SD_ERROR_INTERNAL; + } else if (!write_blob(save_param_path(directory), &it->second.param, sizeof(it->second.param))) { + status = SD_ERROR_INTERNAL; + } else { + it->second.dirty = false; + } + if (option == 1 || !report_event) { + return status; + } + char title_text[sizeof(SceSaveDataTitleId)] = {}; + char dir_text[sizeof(SceSaveDataDirName)] = {}; + std::snprintf(dir_text, sizeof(dir_text), "%s%s", SD_MEMORY_PREFIX, + slot_id == 0 ? "" : std::to_string(slot_id).c_str()); + SceSaveDataTitleId title {}; + SceSaveDataDirName directory_name {}; + std::memcpy(title.data, title_text, sizeof(title.data)); + std::memcpy(directory_name.data, dir_text, sizeof(directory_name.data)); + queue_event_locked(st, SD_EVENT_MEMORY_SYNC_END, user_id, &title, &directory_name, status, flag); + return SD_OK; +} + +int commit_internal(const SaveDataCommitParam* param) { + State& st = state(); + if (param == nullptr) { + return SD_ERROR_PARAMETER; + } + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + std::lock_guard lock(st.mutex); + for (auto& [key, slot] : st.memory) { + if (!slot.dirty) { + continue; + } + const std::size_t sep = key.find(':'); + const std::int32_t user_id = static_cast(std::strtol(key.c_str(), nullptr, 10)); + const std::uint32_t slot_id = sep == std::string::npos + ? 0u + : static_cast(std::strtoul(key.c_str() + sep + 1, nullptr, 10)); + const std::filesystem::path directory = memory_directory(user_id, slot_id); + if (!write_blob(directory / "memory.dat", slot.data.data(), slot.data.size()) || + !write_blob(save_param_path(directory), &slot.param, sizeof(slot.param))) { + return SD_ERROR_INTERNAL; + } + slot.dirty = false; + } + if ((param->commit_mode & SD_COMMIT_BACKUP_ASYNC) != 0) { + queue_event_locked(st, SD_EVENT_COMMIT_BACKUP_END, 0, nullptr, nullptr, SD_OK, nullptr); + } + return SD_OK; +} + +int prepare_internal(const SaveDataMountPoint* mount_point, const SaveDataPrepareParam* param) { + if (mount_point == nullptr || param == nullptr || + !bounded_string_ok(mount_point->data, sizeof(mount_point->data))) { + return SD_ERROR_PARAMETER; + } + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + return SD_OK; +} + +int backup_internal(const SaveDataBackup* backup, KernelEventFlag flag) { + State& st = state(); + if (backup == nullptr || backup->dir_name == nullptr || + !bounded_string_ok(backup->dir_name->data, sizeof(backup->dir_name->data))) { + return SD_ERROR_PARAMETER; + } + if (backup->title_id != nullptr && !bounded_string_ok(backup->title_id->data, sizeof(backup->title_id->data))) { + return SD_ERROR_PARAMETER; + } + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (backup->user_id < 0) { + return SD_ERROR_INVALID_LOGIN_USER; + } + std::lock_guard lock(st.mutex); + queue_event_locked(st, SD_EVENT_BACKUP_END, backup->user_id, backup->title_id, backup->dir_name, + SD_OK, flag); + return SD_OK; +} + +int transferring_mount_internal(const SaveDataTransferringMount* mount, + SaveDataMountResult* mount_result) { + if (mount_result != nullptr) { + std::memset(mount_result, 0, sizeof(*mount_result)); + } + State& st = state(); + if (mount == nullptr || mount_result == nullptr || mount->title_id == nullptr || + mount->dir_name == nullptr || + !bounded_string_ok(mount->title_id->data, sizeof(mount->title_id->data)) || + !bounded_string_ok(mount->dir_name->data, sizeof(mount->dir_name->data))) { + return SD_ERROR_PARAMETER; + } + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + if (mount->user_id < 0) { + return SD_ERROR_INVALID_LOGIN_USER; + } + if (!valid_dir_name(mount->title_id->data) || !valid_dir_name(mount->dir_name->data)) { + return SD_ERROR_PARAMETER; + } + std::lock_guard lock(st.mutex); + const std::filesystem::path directory = save_root() / "transfer" / mount->title_id->data / + mount->dir_name->data; + std::error_code error; + if (!std::filesystem::is_directory(directory, error) || error) { + return SD_ERROR_NOT_FOUND; + } + const int index = free_slot_locked(st); + if (index == -1) { + return SD_ERROR_MOUNT_FULL; + } + for (const auto& slot : st.slots) { + if (slot.used && slot.directory == directory) { + return SD_ERROR_BUSY; + } + } + const std::string mount_point = mount_point_for(save_alias_locked( + st, std::string(mount->title_id->data) + "-" + mount->dir_name->data)); + if (mount_point.size() + 1 > sizeof(mount_result->mount_point)) { + return SD_ERROR_PARAMETER; + } + MountSlot& slot = st.slots[static_cast(index)]; + slot.used = true; + slot.mount_point = mount_point; + slot.dir_name = mount->dir_name->data; + slot.directory = directory; + std::memcpy(mount_result->mount_point.data, mount_point.c_str(), mount_point.size() + 1); + mount_result->required_blocks = 0; + mount_result->mount_status = 0; + return SD_OK; +} + +int get_event_result_internal(const void* event_param, SaveDataEvent* event) { + (void)event_param; + State& st = state(); + if (event == nullptr) { + return SD_ERROR_PARAMETER; + } + std::lock_guard lock(st.mutex); + if (st.events.empty()) { + return SD_ERROR_NOT_FOUND; + } + const PendingEvent pending = st.events.front(); + st.events.pop_front(); + if (pending.flag != nullptr) { + sceKernelDeleteEventFlag(pending.flag); + } + *event = pending.record; + return event->error_code; +} + +int cancel_internal(KernelEventFlag event) { + State& st = state(); + if (event == nullptr) { + return SD_ERROR_PARAMETER; + } + std::lock_guard lock(st.mutex); + for (auto it = st.events.begin(); it != st.events.end(); ++it) { + if (it->flag == event) { + sceKernelCancelEventFlag(event, 0, nullptr); + const KernelEventFlag flag = it->flag; + st.events.erase(it); + sceKernelDeleteEventFlag(flag); + return SD_OK; + } + } + return SD_ERROR_NOT_FOUND; +} + +int abort_internal() { + State& st = state(); + std::lock_guard lock(st.mutex); + for (auto& pending : st.events) { + if (pending.flag != nullptr) { + sceKernelCancelEventFlag(pending.flag, 0, nullptr); + sceKernelDeleteEventFlag(pending.flag); + pending.flag = nullptr; + } + } + st.events.clear(); + return SD_OK; +} + +int create_transaction_resource_internal(std::uint32_t size) { + State& st = state(); + std::lock_guard lock(st.mutex); + if (st.next_transaction_resource <= 0) { + return SD_ERROR_OUT_OF_MEMORY; + } + const std::int32_t id = st.next_transaction_resource++; + st.live_transaction_resources.insert(id); + return id; +} + +int delete_transaction_resource_internal(std::int32_t resource) { + State& st = state(); + std::lock_guard lock(st.mutex); + return st.live_transaction_resources.erase(resource) != 0 ? SD_OK : SD_ERROR_PARAMETER; +} + +std::filesystem::path app_status_path(const std::string& dir_name) { + return save_root() / "_app_status" / dir_name; +} + +int get_local_storage_app_status_internal(const SceSaveDataDirName* dir_name, + const SceSaveDataTitleId* title_id, + std::uint32_t* status) { + if (status == nullptr || dir_name == nullptr || + !bounded_string_ok(dir_name->data, sizeof(dir_name->data)) || + (title_id != nullptr && !bounded_string_ok(title_id->data, sizeof(title_id->data)))) { + return SD_ERROR_PARAMETER; + } + State& st = state(); + std::lock_guard lock(st.mutex); + *status = SD_APP_STATUS_NONE; + std::error_code error; + const std::filesystem::path path = app_status_path(dir_name->data); + if (std::filesystem::is_regular_file(path, error) && !error) { + std::uint32_t stored = 0; + std::ifstream file(path, std::ios::binary); + file.read(reinterpret_cast(&stored), sizeof(stored)); + if (!file) { + return SD_ERROR_BROKEN; + } + *status = stored; + return SD_OK; + } + const std::string alias = save_alias_locked(st, dir_name->data); + if (std::filesystem::is_directory(save_directory(alias), error) && !error) { + *status = SD_APP_STATUS_LOCAL; + return SD_OK; + } + *status = SD_APP_STATUS_NONE; + return SD_OK; +} + +int set_local_storage_app_status_internal(const SceSaveDataDirName* dir_name, + const SceSaveDataTitleId* title_id, + std::uint32_t status) { + if (dir_name == nullptr || !bounded_string_ok(dir_name->data, sizeof(dir_name->data)) || + (title_id != nullptr && !bounded_string_ok(title_id->data, sizeof(title_id->data))) || + (status & ~(SD_APP_STATUS_DELETED | SD_APP_STATUS_LOCAL | SD_APP_STATUS_ONLINE)) != 0) { + return SD_ERROR_PARAMETER; + } + std::error_code error; + std::filesystem::create_directories(app_status_path(dir_name->data).parent_path(), error); + if (error) { + return SD_ERROR_INTERNAL; + } + return write_blob(app_status_path(dir_name->data), &status, sizeof(status)) ? SD_OK + : SD_ERROR_INTERNAL; +} + +int delete_local_storage_app_status_internal(const SceSaveDataDirName* dir_name, + const SceSaveDataTitleId* title_id) { + if (dir_name == nullptr || !bounded_string_ok(dir_name->data, sizeof(dir_name->data)) || + (title_id != nullptr && !bounded_string_ok(title_id->data, sizeof(title_id->data)))) { + return SD_ERROR_PARAMETER; + } + std::error_code error; + std::filesystem::remove(app_status_path(dir_name->data), error); + return error ? SD_ERROR_INTERNAL : SD_OK; +} + +int check_internal() { + if (!is_ready()) { + return SD_ERROR_NOT_INITIALIZED; + } + std::error_code error; + return ensure_save_root(error) ? SD_OK : SD_ERROR_INTERNAL; +} + +std::uint64_t get_all_size_internal(std::int32_t user_id) { + State& st = state(); + std::lock_guard lock(st.mutex); + std::uint64_t total = 0; + for (const auto& [key, slot] : st.memory) { + total += slot.data.size() * ((slot.option & SD_MEMORY_DOUBLE_BUFFER) != 0 ? 2u : 1u); + } + return total; +} + +} // namespace +} // namespace savedata + +#endif diff --git a/core/libs/prx/libSceSaveDataDialog/Export.cpp b/core/libs/prx/libSceSaveDataDialog/Export.cpp index 1b0bcb415..068c96f14 100644 --- a/core/libs/prx/libSceSaveDataDialog/Export.cpp +++ b/core/libs/prx/libSceSaveDataDialog/Export.cpp @@ -1,55 +1,67 @@ #include +#include #include +#include #include "SceTypes.hpp" +#include "prx/libSceSaveDataDialog.native/SaveDataDialog.hpp" #include "prx/libc/include/General.hpp" + +namespace { +// Throwing from these reaches std::terminate and aborts the game process. +int SoftFailNp(const char* func) { + std::fprintf(stderr, "[SOFT-NP] %s -> 0 (unimplemented SCE API - no-op)\n", func); + std::fflush(stderr); + return 0; +} +} // namespace + extern "C" { int APS5_VABI sceSaveDataDialogClose(const void* close_param) { (void)close_param; - NotImplemented_nid_no_patch(__func__); - return 0; + return SoftFailNp(__func__); } int APS5_VABI sceSaveDataDialogGetResult(void* result) { - (void)result; - NotImplemented_nid_no_patch(__func__); - return 0; + // Never return 0 with an untouched out-struct: the game would assert on the garbage later. + if (result == nullptr) { + return SAVE_DATA_DIALOG_ERROR_ARG_NULL; + } + auto* r = static_cast(result); + std::memset(r, 0, sizeof(*r)); + r->result = SAVE_DATA_DIALOG_RESULT_OK; + r->button_id = SAVE_DATA_DIALOG_BUTTON_ID_OK; + return SoftFailNp(__func__); } int APS5_VABI sceSaveDataDialogInitialize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SoftFailNp(__func__); } int APS5_VABI sceSaveDataDialogIsReadyToDisplay(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SoftFailNp(__func__); } int APS5_VABI sceSaveDataDialogOpen(const void* param) { (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; + return SoftFailNp(__func__); } int APS5_VABI sceSaveDataDialogProgressBarInc(int target, uint32_t delta) { (void)target; (void)delta; - NotImplemented_nid_no_patch(__func__); - return 0; + return SoftFailNp(__func__); } int APS5_VABI sceSaveDataDialogProgressBarSetValue(int target, uint32_t rate) { (void)target; (void)rate; - NotImplemented_nid_no_patch(__func__); - return 0; + return SoftFailNp(__func__); } int APS5_VABI sceSaveDataDialogTerminate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SoftFailNp(__func__); } }