Persist SaveDataMemory2 save data to disk (fixes Astro Bot progress not saving) (#659)

* Persist save-data memory across restarts

* Fix SetSaveDataMemory2 data loss, split saves per user, add atomic writes everywhere

- SaveDataSetSaveDataMemory2 treated data_num=0 as "no entries" even when
  a valid data pointer was supplied. Astro Bot calls this with data_num=0
  meaning "one entry, at data[0]" (an implicit-count convention), so every
  real save write from it was silently dropped as a no-op. Default
  data_num to 1 when data is non-null, matching that convention.
- SaveDataSetupSaveDataMemory2/Get/SetSaveDataMemory2 rejected any call
  that included a non-null icon pointer. Icon persistence isn't
  implemented, but titles are allowed to pass one anyway; rejecting the
  whole call over it made every title that does so fail outright. Ignore
  icon instead of rejecting.
- SaveDataMount3/SaveDataTransferringMount/SaveDataDelete built their
  save-data path from title+dir_name only, dropping the user_id PS5
  local users are keyed by (unlike the newer SetSaveDataMemory2 layout,
  which already nests by user_id). Nest these under user_id too so saves
  from different local users can no longer collide. No migration for the
  old flat layout - the project is still pre-release/experimental.
- Classic SaveDataMount3-mounted titles (Blasphemous, Dead Cells, ...)
  write their save files directly through the guest's normal file I/O
  once mounted, bypassing SaveData's own write path entirely, so they
  had none of the crash-safety SetSaveDataMemory2 already had. Added the
  same protection at the file-system layer: a guest file opened under a
  /savedataN mount that recreates the file from scratch (O_CREAT and/or
  O_TRUNC) is now written to a .tmp shadow file and atomically replaces
  the real one on close, instead of being truncated in place.
- Both write paths now back up the file they're about to replace into a
  sibling "backups" folder (same filename, no added suffix) before the
  atomic replace, so a manual recovery copy of the previous save always
  survives a write - even one that "succeeds" but wasn't wanted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Fix save-memory compatibility and preserve existing saves

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Çağatay Güçlü
2026-09-19 23:51:22 +03:00
committed by GitHub
parent f100f785da
commit cfbaee07f0
4 changed files with 773 additions and 53 deletions
+28
View File
@@ -89,6 +89,33 @@ void CheckSaveRename(const std::filesystem::path &root,
"renamed save contents");
}
void TestSaveOpenVisibility() {
constexpr char Path[] = "/savedata0/visible-save.dat";
constexpr char Payload[] = "saved progress";
const int fd = FileSystem::KernelOpen(Path, 0xa01, 0777);
Check(fd >= 3, "create save file exclusively");
FileSystem::FileStat stat {};
Check(FileSystem::KernelStat(Path, &stat) == OK && stat.st_size == 0,
"created save file is visible before close");
Check(FileSystem::KernelOpen(Path, 0xa01, 0777) ==
Libs::LibKernel::KERNEL_ERROR_EEXIST,
"exclusive creation detects an open save file");
Check(FileSystem::KernelWrite(fd, Payload, sizeof(Payload) - 1) ==
sizeof(Payload) - 1,
"populate save file before truncation");
Check(FileSystem::KernelClose(fd) == OK, "close populated save file");
Check(FileSystem::KernelStat(Path, &stat) == OK &&
stat.st_size == sizeof(Payload) - 1,
"save file contains the truncation fixture");
const int truncated = FileSystem::KernelOpen(Path, 0x401, 0777);
Check(truncated >= 3, "truncate existing save file");
Check(FileSystem::KernelStat(Path, &stat) == OK && stat.st_size == 0,
"save truncation is visible before close");
Check(FileSystem::KernelClose(truncated) == OK, "close truncated save file");
}
void CheckMountRoot(const std::filesystem::path &root) {
Common::File cache;
Check(cache.Create(root / "rpf.cache"), "create directory listing fixture");
@@ -390,6 +417,7 @@ int main() {
CheckDirectoryStream(temporary.Path());
CheckAprPaths(temporary.Path());
FileSystem::Mount(temporary.Path(), "/savedata0");
TestSaveOpenVisibility();
CheckSaveRename(temporary.Path(), "first-save");
CheckSaveRename(temporary.Path(), "replacement-save");
FileSystem::Shutdown();