#5173 changes the create condition right after the last error check, and #3950 removes the comment after the create block, so clearing the result in either place made the pull requests conflict. The result is still cleared only after every error return and before anything is written to it.
Every native save reported SAVE_DATA_BLOCKS_MAX blocks, all of them free,
in the sceSaveDataDirNameSearch infos and in sceSaveDataGetMountInfo,
whatever the title created it with and whatever its files held. A save's
size is now the blocks of the sceSaveDataMount3 call that created it,
kept beside the save; a save without a kept size takes that of its next
mount that names one, and otherwise keeps SAVE_DATA_BLOCKS_MAX. Free
blocks are what the save's files leave of it, each file rounded up to
32 KiB blocks, and the blocks and free-blocks sort keys use both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Animal Well (PPSA22520) checks for its save with a CREATE2 mount it never
unmounts, then saves with a second mount of the same directory. That
mount returns BUSY, and the title keeps using the first mount point for
sceSaveDataPrepare, its save file and sceSaveDataUmount2.
sceSaveDataMount3 cleared the result before its checks, so the save was
written to "/save" in the game folder instead of the save directory, and
the first mount was never released. The result is now written only when
the mount succeeds, as shadPS4 does.
sceSaveDataDirNameSearch reads % in the directory name pattern as any
run of characters and _ as one character. The matcher stopped when the
name ran out and then required the pattern to have ended too, so a
trailing % with nothing left to match failed: SAVEDATA% did not find a
save named SAVEDATA, and TRAI_% did not find TRAIL. A % now matches no
characters at the end of a name as it already did in the middle.
Minecraft (PPSA17221) fails to load on nine absent imports. The authorized-app
dialog runs the common-dialog state machine without UI and reports the user
as having cancelled, like the login and VR setup dialogs. The other two,
sceNpAuthGetAuthorizedAppCode and the unnamed libSceSaveData.native NID
RvID8BVeg+g, are declared as throwing stubs with unknown signatures.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SaveDataDirNameSearchResult.infos points at an array the title reads one
entry per returned directory name, but the search never wrote it, and
SaveDataSearchInfo was declared as 128 opaque bytes. A title that asks for
the infos read whatever its buffer held.
SaveDataSearchInfo is now the 48-byte record shadPS4 declares as
OrbisSaveDataSearchInfo (blocks, free blocks, 32 reserved bytes), and the
search fills one per returned name. Every native save reports
SAVE_DATA_BLOCKS_MAX blocks, as sceSaveDataGetMountInfo and the block sort
keys already assume, so both counts are SAVE_DATA_BLOCKS_MAX and the
reserved bytes are zero.
guest_savedata_search_tests checks the two records, their zero reserved
bytes and that the search writes nothing past them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFVE5WbaUJ2vyj8NAW44CS
PPSA28416 calls it on its existing save at boot (after sceSaveDataDirNameSearch finds it) and aborted on the missing implementation. The call takes one parameter block: user id at +0, the directory name pointer at +8. Host saves have no size limit, so the conversion completes at once: an existing save returns OK, a missing one NOT_FOUND, an invalid name PARAMETER. The title polls sceSaveDataGetEventResult once afterwards and continues on 'no event'.
The search returned saves in host directory order and ignored the condition's key and order. A title that asks for the newest save first (modification time, descending) got whichever directory the host listed first: the alphabetically first on NTFS, an arbitrary one on Linux. Sort by directory name, user parameter or modification time, reverse for a descending order, and reject unknown keys and orders with the parameter error, as shadPS4 does.
PPSA30601 (Resonance: A Plague Tale Legacy) fails to load without these imports that no library exports. Their signatures are unknown, so they are declared without parameters and throw until they are implemented. Adds a TechnicalDebt entry.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Palworld (PPSA23126) imports sceSaveDataCancel (e6y3WMBYbiM) from
libSceSaveData and fails to load because no library exports it. Its
signature is unknown, so it is exported without an implementation and
throws if it is called, like sceSaveDataConvert.
sceSaveDataMount3 with SCE_SAVE_DATA_MOUNT_MODE_CREATE2 opens a save that
already exists, so its mount status is 0 rather than CREATED, as shadPS4's
savedata.cpp reports. A CREATE mount still reports CREATED.
Write initial SaveDataParam metadata before replacing the memory blob so metadata failure cannot leave a prematurely grown blob. Handle blob write failure with metadata rollback, and allow retries to recover slots with missing metadata. Expand tests with nonzero data, blob failure, and fresh readback.
Report SAVE_DATA_ERROR_INTERNAL when setupSaveDataMemory2 fails to persist initial SaveDataParam metadata, and restore previous state so retrying setup can initialize cleanly. Closes#1667.
The 36-byte SceSaveDataDirName declaration placed every search result
after the first at the wrong address and could overrun guest arrays.
Skyrim read an empty directory name for its second save and aborted
when mounting it. Use the 32-byte layout also used by the native dialog,
KytyPS5 src/libs/libSaveData.cpp and fpPS4 ps4_libscesavedata.pas.
Add a regression using two maximum-length names, a guest buffer guard,
and mounts of both returned records. It detects the previous overrun.
Fedora 44 Release search, dialog, delete and PS4-save tests pass. Skyrim
now mounts and reads the second save instead of rejecting its name.
Written with AI assistance.
PPSA16064 (Minecraft Dungeons II, Unreal Engine) failed to load because
these imports had no export:
- libkernel: sceCoredumpWriteUserString
- libSceSaveData: sceSaveDataConvert, sceSaveDataGetConvertProgress
- libSceVideoOut: sceVideoOutVrrPegToFixedRate,
sceVideoOutVrrUnpegFromFixedRate
None of them is called during the title's start-up, so they are added
as unimplemented exports that throw with their name if a title calls
them. Their signatures are unknown; TechnicalDebt records them. The
built libraries export the NIDs the title imports.
Every library CMakeLists.txt ended with the same block removing -static,
-static-libgcc and -static-libstdc++ from CMAKE_EXE_LINKER_FLAGS on MinGW,
because core/libs/CMakeLists.txt only removed them after adding the library
directories. Remove them before the libc and library directories are added
so each directory inherits the stripped flags, and drop the 52 copies.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017pTZqAyVSZy2FZmaqakPHZ
Mount, unmount, param, icon and mount-info calls read and wrote the
global mount slot table without a lock, so two threads mounting at
once could take the same slot and one would later fail to unmount its
own mount point. Guard the table with a mutex and make the initialization
count atomic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MBXJqzgohLeVAeHsn4Jpm7
A second sceSaveDataInitialize3 returned SAVE_DATA_ERROR_ALREADY_INITIALIZED.
PPSA12544's executable and its Unity SaveData plugin both initialize, and
the plugin raises Unity.SaveData.PS5.Core.SaveDataException on any error,
so the console must accept the second call. Initializations are now
counted; sceSaveDataTerminate ends the session at the last one and only
then checks for mounted slots.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rename the two unknown libSceSaveData.native stubs to
sceSaveDataTransferringMountPs4 (RjMlsR8EXrw) and
sceSaveDataDirNameSearchPs4 (X4MYzukPc3g). There is no PS4 save area:
the mount returns NOT_FOUND like sceSaveDataTransferringMount, and the
search reports zero hits.
Co-Authored-By: Claude <noreply@anthropic.com>
sceSaveDataDelete appended the title's directory name to the save root
without any check and passed the result to std::filesystem::remove_all.
"../victim" removed a directory beside the save root, ".." the directory
holding the save root, and an empty name or "." every save. The name
now has to be terminated within its field, non-empty, not "." or "..",
and free of "/", "\" and ":", as sceSaveDataMount3 already requires;
otherwise the call returns SAVE_DATA_ERROR_PARAMETER and deletes nothing.
The native library gains its own CMakeLists.txt, with the same library
definition as the generic one, to register guest_savedata_delete.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R5WJS4ZdQynAJesCZKH5hb
A .param file of a save directory or of save data memory that exists but cannot
be read, or whose size is not sizeof(SaveDataParam), was silently ignored or
truncated. It now throws, naming the file and, for a size mismatch, both sizes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sceSaveDataSetParam dropped the params, and sceSaveDataGetParam and
sceSaveDataDirNameSearch returned zeros, so a title that checks the params of
the save it found took its profile for a new one and rewrote it with defaults
on every boot. The params now live in a <directory>.param sidecar next to the
save directory, written by SetParam (mtime refreshed) and returned by GetParam
and by the directory search, with the directory's write time when no param
was stored yet.
The mount point is /savedata<N>, as on the console: a title read its profile
through /host//_sm/0/Profile when the mount point had another shape, and that
path does not exist.
The module logs the first three calls of each entry point, like the core
module; APS5_SAVEDATA_TRACE lifts the limit.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>