1. 三类状态不能混为一谈#
游戏运行对象、可保存的结构化数据和已经写入设备/云端的文件是三个不同层次。CGenericGameStorage 组织整体操作,SaveGameBuffer 和各数据结构采集/恢复游戏状态,操作队列与状态机负责设备、用户、错误和异步 IO。
统计也不是直接写一个文件:StatsMgr 收集玩法数据,StatsDataMgr 管理值和脏状态,StatsSavesMgr 决定何时保存或刷新到 profile/cloud。照片、单机存档、多人角色/公共数据又有不同生命周期。
2. 保存流程的状态化组织#
图概括多个接口的职责,不声称所有目标都经过完全相同的线程与 IO 顺序。eGenericSaveState 明确列出 autosave 检查、损坏检查、覆盖确认、create/fill buffer、begin/do save、错误处理和失败清理。
CSavegameQueue::Update 只推进队首 operation,完成后断言状态不是 BUSY 再 Drop;Push 和 Update 都断言在 update thread。队列串行化的是逻辑操作,不表示底层 IO 全在主线程。CSaveGameBuffer::BeginSaveBlockData 明确启动 worker,DoSaveBlockData 只能由该工作线程调用。
3. 数据结构而非内存快照#
SavegameData 按玩家、Stats、相机、门、车库、小地图、广播、stunt jump、额外内容和简单变量划分,许多结构有 .psc 定义。单机 buffer 的 ReadDataFromPsoFile 按命名对象恢复这些结构,再读取脚本树。
这是一种有显式保存 schema 的持久化:不会自动保存每个 C++ 对象的所有字段。新功能若只添加 runtime 成员而没有保存/恢复映射,读档后不会自然保留;改变枚举、hash、结构默认值和类型也可能影响旧数据。
代码同时存在 PSO 与 XML 相关路径,二者具体用途和配置需沿调用点确认,不能据此认为所有发行存档都以明文 XML 保存。buffer 还区分是否包含脚本数据和是否使用加密的操作参数,实际启用由调用上下文决定。
4. 读取与会话重建#
加载状态包括初始检查、获取文件大小、分配缓冲、开始/执行读取、等待加载提示结束和错误处理;GenericLoad 与 DeSerialize 分离。文件成功读入不等于世界已恢复,还要结合会话状态机重建关卡和对象。
单机 buffer 有一个关键生命周期细节:文件内容缓冲在 INIT_AFTER_MAP_LOADED 才释放,而非 SHUTDOWN_SESSION。源码注释说明这是为了防止加载时尚未读取的数据在旧会话关闭时被提前清掉。存档跨越旧/新会话,不能机械套用“会话结束释放所有数据”。
脚本保存支持数组、结构、嵌套、栈和 PSO file tracking。这与 Native 的 BEGIN/STOP 类构造协议相接,GtaThread 执行后也会检查未闭合保存操作。持久化正确性取决于声明/采集与运行时恢复一致,而非只检查文件写入返回值。
5. 统计的多种脏状态#
StatsDataMgr.h 明确区分:
| 标志 | 含义 |
|---|---|
| Dirty | 值发生变化 |
| DirtyProfile | 变化需要反映到 profile stats |
| DirtySavegame | 变化需保存到存档,完成保存才清除 |
| DirtyInThisSession | 本会话是否变化过 |
| NoSaveStat | 仅本会话使用、不需持久保存 |
这些位不能相互替代。值已发送到某个服务,不一定意味着本地存档已落盘;清除一般 Dirty 也不应顺便抹去待保存信息。
StatsSavesMgr 记录保存历史、是否可在 job 中保存、延后 profile flush、延后 cloud save,以及只处理其中一类的策略。延后合并可以降低频繁写入,但退出、断线和切角色时需要明确剩余脏数据的归属与收尾。
6. 多人、本地/云端与照片#
CGenericGameStorage 同时保存 SinglePlayer、MultiplayerCharacter 和 MultiplayerCommon buffer,后两者又有 cloud/local 实例。多人角色数据与公共数据分离,有利于不同保存粒度;但冲突、回退和同步策略不能简单推断为“最后写入覆盖”。
照片系统包括本地保存/加载、云列表/删除、合并列表、元数据、唯一 ID、异步缓冲和 operation queue。照片缩略图纹理、照片文件、云元数据与游戏相册不是一个生命周期,界面退出时仍需正确处理进行中的 IO。
MoneyInterface 与网络 Shop/Transactions 连接游戏经济。统计数值可用于显示和记录,但服务端交易结果不能由随意修改本地 stat 代替;经济持久化需沿交易协议理解。
7. SaveMigration 的独立责任#
CSaveMigration 持有 singleplayer 和 multiplayer manager。单机有查询存档状态、上传和下载任务,多人包含账户与 migration 状态请求。它处理跨环境/账户相关的迁移服务流程,与普通运行时 schema 的局部字段兼容不是同一层问题。
此外 SaveLoad/SavegameMigration 也有与存档迁移相关的支持文件。阅读时要从入口与职责区分,不能把两个目录当作重复代码。
客户端请求接口只能证明支持相应服务交互,不能证明当前账号、平台或后端仍提供该能力;本文未发出任何云端请求。
8. 维护与验证#
重点应覆盖保存期间数据继续变化、队列满、设备/用户变化、云端失败、旧会话关闭、脚本强制终止、旧 schema 数据与照片 UI 生命周期。成功写文件、成功编码和成功恢复玩法都需要分别判断。
本次未读取用户实际存档,也未执行保存、迁移或数据库操作。对错误处理和兼容性的描述是静态结构分析。
9. 源码依据#
| 源码 | 重点 |
|---|---|
| GenericGameStorage.h | 单机/多人/local/cloud buffer 和统一操作 |
| savegame_queue.cpp | 线程约束、队首推进、完成出队 |
| savegame_save.h | 保存状态机 |
| savegame_load.h | 加载状态机与反序列化 |
| savegame_buffer.h | 保存块 worker 与内存所有权 |
| savegame_buffer_sp.cpp | PSO 数据恢复、跨会话缓冲生命周期 |
| StatsDataMgr.h | 多种 dirty 标志 |
| StatsSavesMgr.h | 延后与分类保存策略 |
| SaveMigration.h | SP/MP 迁移入口 |