给 Sudachi 写存档编辑器:从路径定位到安全回写的完整做法
【免费下载链接】sudachiSudachi is a Nintendo Switch emulator for Android, Linux, macOS and Windows, written in C++项目地址: https://gitcode.com/GitHub_Trending/suda/sudachi
给 Sudachi(C++ 编写的 Switch 模拟器)做一个 SaveData 存档编辑器,目标很明确:给定一个用户和一款游戏,你能定位到它的存档目录、读出容量元数据、把修改安全写回去。Sudachi 的寻址规则集中在SaveDataFactory一个类里,读懂它,编辑器就完成了一半。全文按"先能读 → 再能读对 → 最后能安全改"三步推进。
准备清单:开工前该读的文件
- src/core/file_sys/savedata_factory.cpp:
GetFullPath、新旧位置判定、大小文件读写都在这一个文件里 - src/core/file_sys/savedata_factory.h:类接口,以及
.sudachi_save_size文件名常量的定义处 - src/core/file_sys/fs_save_data_types.h:
SaveDataSpaceId、SaveDataType枚举,SaveDataSize与UserId结构体 - 可选:
src/core/file_sys/vfs/目录,了解GetDirectoryRelative这类句柄获取方式
任务一:能读——用户存档路径怎么拼出来 🔍
要解决什么:输入user_id和title_id,输出一个能直接打开的目录路径。
关键原理:路径 = 空间前缀 + 类型相关段。三个空间各有固定前缀:System 是/system/,User 是/user/,Temporary 是/temp/。你日常打交道的 User 空间里,Account(账号档)和 Device(设备档)走同一条拼接分支:固定段0+ 用户 UUID + title_id。user_id是 128 位的u128,拆成高低两个 64 位各格式化出 16 位十六进制,目录名正好 32 位。
// out 为空间前缀,例如 "/user/";Account 与 Device 同分支 case SaveDataType::Account: case SaveDataType::Device: return fmt::format("{}save/{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0], title_id);其余类型一句话带过:Cache 只跟 title_id 走(save/cache/<title_id>),Temporary 以 16 位的0开头,System 档用save_id定位。
动手验证:给这个纯字符串函数写单测,断言三件事——固定user_id时输出目录名恰好 32 位十六进制;title_id传 0 时应被替换成程序自身 ID 再拼接;输出以/user/save/0/开头。
任务二:能读对——新旧两代目录如何避免漏档 🧭
要解决什么:同一个存档可能躺在"旧位置"或"未来位置",你的编辑器必须知道它实际在哪。
关键原理:GetFullPath走主分支之前,会先算一条新格式路径并检查它是否存在。新格式只覆盖 Account 和 Device:前者是/user/save/account/<uuid>/<title_id>/0,后者是/user/save/device/<title_id>/0(uuid 为标准 UUID 字符串)。只要新目录存在就直接返回新路径,不再回退旧位置。注意匹配前还做了title_id & ~(0xFFULL),即把标题 ID 最低字节清零再比较。
if (std::string future_path = GetFutureSaveDataPath(space, type, title_id & ~(0xFFULL), user_id); !future_path.empty()) { if (const auto future_dir = dir->GetDirectoryRelative(future_path); future_dir != nullptr) { return future_path; // 新位置已存在,优先使用 } }两代根目录各有现成函数GetUserGameSaveDataRoot(user_id, future)生成:future为 false 时是/user/save/0/<32 位 UUID>,为 true 时是/user/save/account/<uuid>。
动手验证:构造两个测试目录,一个按旧格式、一个按新格式放入同名存档。断言:旧格式场景返回旧路径;新格式场景返回account/开头的路径;两者并存时只认新路径。
任务三:能安全改——大小文件 .sudachi_save_size 怎么读写 🔒
要解决什么:改存档前你得知道它声明的容量,改完后这份元数据不能失真。
关键原理:每个存档目录里有一个 16 字节的元数据文件.sudachi_save_size(注意是点开头)。内容是一个SaveDataSize结构体:normal和journal两个u64,分别记录常规区与日志区大小。读的逻辑很短:文件不存在或不足 16 字节,一律按{0, 0}处理;够长就直接按内存布局读成结构体。
// 读:文件缺失或不足 16 字节,统一按空值处理 if (size_file == nullptr || size_file->GetSize() < sizeof(SaveDataSize)) { return {0, 0}; } // 写:先 Resize 到 16 字节,再整体写结构体 size_file->Resize(sizeof(SaveDataSize)); size_file->WriteObject(new_value);还有一条写入护栏:别假设"打开即创建"。Open时目录不存在是否顺手创建,由ShouldSaveDataBeAutomaticallyCreated判定——Cache、Temporary 恒允许;User 空间下的 Account/Device 档只在program_id与system_save_data_id均为 0 时允许。System 档和带明确 ID 的存档一律不行。所以你的工具要区分只读浏览与写入两种模式,写入前先把.sudachi_save_size复制一份.bak。
动手验证:跑一次"读 → 改 → 回读"往返:写入一个已知SaveDataSize,回读必须逐字段一致。再制造一个 8 字节的坏文件,断言读回{0, 0}且不抛异常。最后实测.bak恢复流程能还原原始 16 字节。集成测试可放在src/tests/下,用模拟器真实存档跑同一往返。
常见问题
title_id 为 0 时,为什么拼出的路径对不上
原因:Account/Device 请求里title_id为 0 表示"当前进程",模拟器会先回填成program_id再拼路径。正确做法:编辑器拿到元数据后先做同样的替换,别把 0 当合法 ID 拼进目录名。
迁移扫描为什么只查旧目录会漏档
原因:新位置存在时模拟器优先使用新位置,旧目录可能早已空掉。正确做法:新旧两代根目录都扫,任一路径命中即算找到,并在界面标注当前代次。
大小文件不足 16 字节算不算损坏
原因:模拟器对"缺失或截断"一律按{0, 0}兜底,它代表未初始化而不是损坏。正确做法:读侧照抄这个兜底逻辑,写侧在动数据前先留.bak。
为什么不能指望"打开即创建"目录
原因:自动创建只覆盖 Cache/Temporary,以及两个 ID 全为 0 的 User 空间 Account/Device 档;System 档与带明确 ID 的存档不在其列,误建目录会污染存档树。正确做法:浏览模式保持只读,确需目录时显式调用Create。
多用户存档目录名为什么长得像
原因:目录名是user_id高 64 位在前、低 64 位在后的 32 位十六进制,肉眼难分,顺序写反还会指错目录。正确做法:展示时转成标准 UUID 字符串,存储统一按user_id[1]+user_id[0]顺序拼接。
交付前自查清单
- 路径拼接单测全绿:固定 UUID、
title_id=0 回填、最低字节清零三组断言都通过 - 新旧两代根目录同时扫描,命中集合与模拟器实际读取一致,且界面标注了位置代次
.sudachi_save_size往返一致:写入已知值回读逐字段相同,坏文件读回{0, 0}.bak备份与恢复流程实测可还原原始 16 字节- 浏览模式下 System 档和带 ID 存档不产生任何新目录
扩展方向留三个口子:云同步可对接src/network/的房间机制,加密档可复用xts_archive与 crypto 模块,批量迁移按"用户 UUID → 设备"做模板映射;界面栈直接沿用 Sudachi 桌面端的 Qt,配QTreeView和现成十六进制控件最省心。
存档编辑器做到最后,拼的是你对SaveDataFactory寻址规则的还原度:双代路径读全、16 字节元数据写准、自动创建查严——顺序反了就得返工。
【免费下载链接】sudachiSudachi is a Nintendo Switch emulator for Android, Linux, macOS and Windows, written in C++项目地址: https://gitcode.com/GitHub_Trending/suda/sudachi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考