Sudachi存档编辑器:从零搭建SaveData修改工具
【免费下载链接】sudachiSudachi is a Nintendo Switch emulator for Android, Linux, macOS and Windows, written in C++项目地址: https://gitcode.com/GitHub_Trending/suda/sudachi
Sudachi 是一款用 C++ 编写的跨平台 Nintendo Switch 模拟器,它通过SaveDataFactory统一管理游戏存档的存放与访问。如果你遇到过"换了账号就找不到存档"或"存档坏了打不开"的情况,问题大概率出在路径映射和大小元数据上。这篇文章带你顺着源码把存档编辑器从零搭起来。
一条存档路径是怎么拼出来的
Sudachi 的存档不放在扁平结构里,而是按"空间 → 类型 → 标识"三级目录组织。顶层由SaveDataSpaceId(存档所在空间)决定:System 对应/system/,User 对应/user/,Temporary 对应/temp/。平时要动的用户存档都在/user/save/下。
关键在于user_id参数:它是一个 128 位 UUID,可以理解为由两个 64 位整数组成的 128 位大整数,代表玩家唯一身份。生成路径时拆成user_id[1]和user_id[0],各自格式化为 16 位十六进制字符串再拼起来。
GetFullPath(位于 savedata_factory.cpp)的核心分支按存档类型决定最终路径:
// src/core/file_sys/savedata_factory.cpp 中 GetFullPath 的核心分支 switch (type) { case SaveDataType::System: return fmt::format("{}save/{:016X}/{:016X}{:016X}", out, save_id, user_id[1], user_id[0]); case SaveDataType::Account: case SaveDataType::Device: return fmt::format("{}save/{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0], title_id); case SaveDataType::Temporary: return fmt::format("{}{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0], title_id); case SaveDataType::Cache: return fmt::format("{}save/cache/{:016X}", out, title_id); }从这段代码能看出用户存档的典型形态:/user/save/0/<32位user_id>/<16位title_id>/。工具里定位某个玩家的存档根目录,直接用GetUserGameSaveDataRoot的逻辑即可:0前缀 + 拼接后的 user_id 就是根路径,再往下按 title_id 找具体游戏。
五种存档类型与自动创建规则
SaveDataType(区分存档归属的枚举)共 5 个成员,路径特征和权限各不相同:
System:系统级存档,路径里带save_id和 user_id,需系统权限Account:玩家维度存档,路径含 32 位 user_id 和 title_idDevice:设备共享存档,路径只有 title_idTemporary:会话期临时存档,注意它的 switch 分支里没有save/这一段Cache:缓存存档,统一放在save/cache/下
自动创建的判断在ShouldSaveDataBeAutomaticallyCreated里:Cache 和 Temporary 无条件自动创建;Account 和 Device 只在program_id == 0且system_save_data_id == 0时才会自动建目录。做修改工具时记住这条规则:不要替系统存档"补建"目录,那是受保护的数据。
解析 .sudachi_save_size 大小文件
每个存档目录里有个隐藏文件.sudachi_save_size(由GetSaveDataSizeFileName()定义,并非某些资料里写的save_size.dat)。结构定义见 fs_save_data_types.h:
// 结构共 0x10 字节:normal 普通区大小,journal 日志区大小 struct SaveDataSize { u64 normal; u64 journal; }; // ReadSaveDataSize 的核心:文件不存在或不足 16 字节时返回 {0, 0} if (size_file->ReadObject(&out) != sizeof(SaveDataSize)) { return {0, 0}; }ReadObject是 vfs.h 里的模板接口,把 16 个字节直接拷进平凡可拷贝的结构体。所以解析这个文件就是"读 16 字节、拆成两个小端 64 位整数";文件缺失或短于 16 字节代表没有大小记录,工具要走{0, 0}分支而不是崩溃。
改之前先备份大小文件
动存档目录里任何字节前,先把大小文件复制一份。VFS 层提供现成的CopyFile:
// 用 VfsFilesystem::CopyFile 把大小文件备份一份,防止改坏 journal 字段 vfs->CopyFile(save_dir + "/.sudachi_save_size", save_dir + "/.sudachi_save_size.bak");写回时照WriteSaveDataSize的组合拳:CreateFile+Resize(sizeof(SaveDataSize))+WriteObject,顺序不能乱。
可视化界面选型:Qt 还是 Python
Sudachi 的桌面前端本身就基于 Qt5/Qt6(见src/sudachi/CMakeLists.txt的构建脚本),所以做跨平台编辑器选 Qt 阻力最小:QTreeView 做目录导航,QHexEdit 做十六进制编辑,再挂一个小面板做 u64/字符串/十六进制互转。如果更熟悉 Python,用 PyQt 加pathlib遍历目录也行,核心仍是上面那套路径拼接逻辑,界面只是壳。
四个兼容性与安全上的坑
- 新旧两套路径并存:新版 Sudachi 支持 future 格式(
GetFutureSaveDataPath),用户存档会放在/user/save/account/<uuid>/<title_id>/0。GetFullPath会优先检测新位置、不存在才回退旧路径,你的扫描逻辑同样要两种格式都试。 - title_id 为 0 是占位:Account/Device 类型下
title_id == 0表示"当前进程的 title ID",工具拼路径前必须用真实 program ID 替换。 - temp 目录每次启动会被清空:
SaveDataFactory构造时直接调用DeleteSubdirectoryRecursive("temp"),别把临时存档当持久修改目标。 - 别碰系统存档:空间在
/system/下,路径规则也不同,误写可能直接破坏模拟器状态。
测试验证:从路径单测到启动回归
先给路径生成写单元测试:把 5 种类型 × 新旧空间格式作为输入组合,用EXPECT_EQ断言生成的字符串,测试文件可放进src/tests/现有结构。然后做集成测试:准备一份从模拟器导出的真实存档目录,验证工具能正确读出.sudachi_save_size,修改若干字节后写回再重读,最后启动游戏确认存档可正常加载。
下一步
- 先实现
GetFullPath与GetUserGameSaveDataRoot的双格式逻辑,把路径单测跑绿; - 接上 QTreeView + QHexEdit,打通"浏览 → 定位 → 编辑 → 写回"主流程;
- 给大小文件加自动
.bak备份,并对/system/下目录禁写; - 需要处理加密卷存档时,再顺着
src/core/file_sys/system_archive/的 NCA/XTS 相关代码扩展解密能力。
【免费下载链接】sudachiSudachi is a Nintendo Switch emulator for Android, Linux, macOS and Windows, written in C++项目地址: https://gitcode.com/GitHub_Trending/suda/sudachi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考