AnyPS5存档系统完全解析:libSceSaveData如何在PC上读写PS5存档文件
【免费下载链接】AnyPS5Tool for automatic PS5 executables porting to Linux and Windows项目地址: https://gitcode.com/GitHub_Trending/an/AnyPS5
如果你想在电脑上玩移植的PS5游戏,AnyPS5是最值得关注的开源方案之一——它把PS5可执行文件自动移植到 Linux 和 Windows。而在整个移植过程中,最难啃的骨头之一就是存档系统:游戏在主机上调用sceSaveDataMount3、sceSaveDataSetParam等 API 读写存档,这些接口原本依赖PS5的文件系统和PSN用户体系。AnyPS5 用一套名为libSceSaveData的纯本地文件实现把它完整搬到了PC上,不需要任何PSN账号。下面带你彻底看懂它的存档系统是如何落地的。
📂 存档到底存在哪:_sd目录结构
libSceSaveData 把所有存档都放进一个统一的根目录/_sd(由 SaveDataCore.hpp 中的SD_ROOT_GUEST定义,实际物理路径由运行环境的目录解析器决定)。每个存档就是一个子目录,结构非常直观:
| 路径 | 作用 |
|---|---|
_sd/<存档名>/ | 一个存档目录,存放游戏数据 |
_sd/<存档名>/sce_sys/param.bin | 存档元数据(标题、副标题、详情、修改时间等) |
_sd/<存档名>/sce_sys/blocks.bin | 8 字节的"块数"记录,模拟主机上的空间配额 |
_sd/_aliases | 长目录名的短名别名表(制表符分隔) |
_sd/_app_status/<存档名> | 4 字节的应用状态标志(本地/已删除/在线) |
这种布局刻意模仿了PS5真实主机上的sce_sys/param.bin结构,因此游戏里"读取存档列表、显示存档标题和游玩时间"的界面在PC上也能原样工作。
⚙️ 挂载机制:16个挂载槽 + 短名别名
PS5 API 要求游戏通过"挂载"打开存档:游戏先请求挂载_sd/<存档名>,系统返回一个最多16字节的挂载点,游戏后续只操作这个短路径。
AnyPS5 的做法是:
- 最多16个挂载槽(
std::array<MountSlot, 16>,见 SaveDataCore.hpp),同一存档不允许被两个槽重复挂载,否则会返回SD_ERROR_BUSY; - PS5 的存档目录名上限是32字符,但挂载点容量只有16字节。当目录名过长时,libSceSaveData 会自动生成"前6字符 +
~XXX哈希后缀"的短别名,并持久化到_aliases文件(实现见 save_alias_locked)。重启后照样能正确还原长目录名; - 挂载模式与主机一致:只读(
SD_MOUNT_RDONLY)、读写(SD_MOUNT_RDWR)、创建(SD_MOUNT_CREATE/CREATE2),新建存档时必须指定1~32768块的空间配额。
更精简的 libSceSaveData.native 版本则用路径别名表(/savedata0、/savedata1…)把短挂载点映射到真实目录,效果相同,实现更薄。
🏷️ param.bin 元数据:标题和"游玩时间"从哪来
游戏列表页显示的存档标题、副标题来自param.bin中的 SaveDataParam 结构(title / sub_title / detail / user_param / mtime 五个字段)。
一个很巧妙的细节:mtime(最后修改时间)并不信任文件本身,而是在每次读取参数时递归扫描整个存档目录,取所有文件的最新写入时间(load_save_param)。这样无论游戏往存档里写什么文件,"上次游玩"时间都会自动更新,无需游戏配合。写入param.bin时则会把 mtime 刷新为当前时间。
💾 内存型存档(sce_sdmemory):高频写入的优化方案
很多现代游戏(尤其是支持"随时保存"的开放世界)不使用文件挂载,而是用内存存档:游戏把整块存档数据(最大32 MiB)放进内存槽,通过sceSaveDataSetupSaveDataMemory2创建、sceSaveDataGetSaveDataMemory2/sceSaveDataSetSaveDataMemory2按偏移读写。
libSceSaveData 支持4个用户 × 4个槽位的内存存档(SaveDataCore.hpp),数据持久化到_sd/sce_sdmemory[槽位]/<用户ID>/memory.dat。每次Commit或Sync时会把脏槽整体刷盘,并推送"同步完成"事件供游戏轮询。native 版本的落盘位置则是_sd_mem/u<用户>/slot<N>.bin(Export.cpp),同一套内存读写 API,两种落盘策略互为补充。
🛡️ 原子写入:为什么存档不会写坏
存档写坏是最令移植项目头疼的问题。libSceSaveData 在 SaveDataFile.hpp 里实现了"临时文件 + 原子替换":先写xxx.tmp,成功后再原子重命名覆盖目标文件;Windows 上则使用MoveFileExW(MOVEFILE_REPLACE_EXISTING)。中途断电或崩溃,最多丢掉一次写入,绝不会留下半截的损坏存档。针对这一机制还有专门的回归测试:SaveDataFileWriteFailure.cpp 和 SaveDataWriteReplacement.cpp。
🧩 API 覆盖度一览
Export.cpp 中的每个入口都委托给SaveDataCore.hpp的本地实现,不再返回"PSN服务不可用"错误(早期版本曾对全部调用返回0x800242e2,对纯本地存档来说是致命的)。核心能力一览:
| 能力 | 状态 | 说明 |
|---|---|---|
| 初始化 / 结束 | ✅ | 支持任意 userId(0 / 65535 / 268435456),无需PSN登录 |
| 挂载 / 卸载存档 | ✅ | 16槽位,长目录名自动别名 |
| 目录搜索(通配符) | ✅ | 支持%与_模式匹配,可返回元数据 |
| 删除存档 | ✅ | 见 GuestSaveDataDelete.cpp 测试 |
| 参数读写 / 图标 | ✅ | 图标接口安全返回空数据 |
| 内存存档 | ✅ | 4槽 × 4用户,32 MiB 上限 |
| 备份事件 | ✅ | 立即生成"备份完成"事件,队列上限64条 |
| PS4存档转移挂载 | ⚠️ | 支持接口调用,真实转移数据按"未找到"处理 |
🧪 测试如何保证可靠
存档系统是游戏数据的地基,所以 AnyPS5 为它准备了一组针对性测试(位于 core/libs/prx/tests/):
- GuestSaveDataDelete.cpp:验证删除行为与边界情况;
- SaveDataMemoryGrowth.cpp:内存存档扩容时的数据保全;
- SaveDataMemoryMetadata.cpp:内存存档元数据一致性。
配合SAVEDATA_TRACE宏(每个入口点打印前3次调用),调试时也能快速确认存档调用链是否走到了本地实现。
📌 小结
AnyPS5 的 libSceSaveData 用不到1500行核心代码,就实现了主机存档系统的完整语义:目录即存档、sce_sys/param.bin存元数据、16字节挂载点、内存槽、原子写盘——游戏代码零修改即可在PC上正常创建、读取、更新和删除存档。对想自己研究或扩展移植行为的开发者来说,SaveDataCore.hpp 就是唯一需要精读的文件;对普通用户而言,只要记住一点:你的所有游戏存档都安全地存放在本地_sd目录里,随时可以整目录拷贝备份。
【免费下载链接】AnyPS5Tool for automatic PS5 executables porting to Linux and Windows项目地址: https://gitcode.com/GitHub_Trending/an/AnyPS5
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考