两文件是流水线流程件而非游戏描述件,此前住在 design/gdd/ 造成三处不适配: gdd_lint 必须为它们逐条关掉体裁规则、格式权威却写在 doc-conventions §12、 与同族的 roadmap(在仓库根)分居两处。迁到根后按 roadmap.md 的无编号风格改名。 - 迁出:design/gdd/06-待办清单.md → 待办清单.md;07-体验验证清单.md → 体验验证清单.md - 指向改写 53 个文件:GDD 各系统文档指路存根、四份规约、02 根索引、README/roadmap/AGENTS - 两份清单自身 74 处出站链接按新基址重指(../art → design/art、systems → design/gdd/systems) - gdd_lint:待办/体验验证两文件的 R1/R3/R5/R7 豁免行随迁删除(已脱离扫描面),后门表清零 - pipeline_gate 默认路径、log_analysis/render_gate 称谓、repo_structure_audit 根文件白名单同步 - 保护面补写:两份清单列入实现批禁写清单(AGENTS 两类批条 / process-conventions §12 / 架构文档域表), 条目状态只由流水线各段推进 - 范围外未动(用户令「全不碰」):reports/ 与 design/research/ 的 22 处历史指向、game/ 两处注释
17 KiB
存档序列化
一句话:存档契约与版本迁移 状态:已定稿(2026-09-21 存档序列化批落地)
设计目的
本系统管的是"关掉游戏之后,玩家的东西还在不在"。序列化(本文含义:把游戏运行中的数据写成可存盘的档案,下次启动再原样读回来)与版本迁移(本文含义:游戏更新后,旧版本写下的存档要能被新版本继续读懂,必要时把旧结构改写成新结构)合起来,就是"存档契约"这门生意的全部。
这件事是核心循环里"存档在变厚"那条故事线的底层承载:钥石攒进存档、倒下也不丢——写在纸面上就是本系统的职责。本系统在切片里的落地面是 core/save_manager.gd(SaveManager,Core 层零 UI、零玩法依赖),本文描述的就是这份契约:存什么、何时读写、版本怎么走、坏了怎么办。
系统范围
边界这样划:本系统只管存档的技术契约面——什么算一份完整的档案、何时读写、旧档怎么跟着版本走。至于存进去的内容归谁定(钥石算什么、摘要记什么),那是各内容系统的口径,本文照单承载,不替任何人定义进度。
负责:
| 负责 | 说明 |
|---|---|
| 存档契约 | 单档 JSON 存档的域划分与读写约定(域清单见核心规则) |
| 版本迁移 | 版本字段 + 读档时链式迁移 + 高版本拒读禁写 |
| 读写时机 | 启动读盘;正式冒险收束写盘;原子写盘原语 |
| 元进度的持久化承载 | 归档登记的职责口径:"元进度持久化框架"——钥石与解锁进度由本系统落盘 |
| 最近冒险摘要的持久化 | 冒险摘要五件事副本入档(cap 10),重开对照跨进程成立 |
| 冒险断点快照的持久化承载 | 停点粒度续玩快照(breakpoint 域)写入/读回/清除/迁移——内容口径归冒险结构(2026-09-29 迭代 8 卡①,P1 预挂号兑现) |
不负责:
| 不负责 | 归谁 |
|---|---|
| 存进去的内容口径(钥石怎么得、能换什么) | 元进度——本系统只承载,不定义 |
| 冒险摘要记哪几件事 | 冒险结构——本系统只消费其摘要五件事 |
| 冒险断点快照记哪些字段、何时算一个合法停点 | 冒险结构——本系统只承载 breakpoint 域的写入/读回/清除(2026-09-29 迭代 8 卡①起,原 P1 预挂号兑现) |
| Boss 击杀的钥石金额计算(首杀全量 / 重复杀小额) | 元进度——本系统只记域内事实,不算账 |
| 数值配方本体(所有玩家一致的表) | 数据配置——表是配方,存档是玩家进度,两者分离(见核心规则) |
| 局内数值的持久化 | 无——局内系统资源全部单局清零,没有局内数值需要入档(见核心规则的反面清单) |
| 系统级设置(音量/画面等) | 不入档——设置与存档两分(2026-09-19 用户拍板):设置独立存储、一安装一份,存档只存生涯数据;设置域归设置与本地化所有 |
在核心循环中的位置
游玩中攒下的进度事实(钥石到手 / 冒险结算)──▶ 存档序列化写盘 ──(下次启动读回)──▶ "存档在变厚"被兑现
存档序列化不占核心循环里的一段,它贴在"死亡重开"出口的外侧:局内胜负归零时,它保证玩家攒下的东西不跟着归零。核心循环把"存档在变厚"讲成玩家重开的理由之一,本系统就是那条理由的底层承载。
核心规则
基础层身份:无上游依赖的承载方
SaveManager 不依赖任何玩法系统就能工作:它是被动的承载方,任何系统要把数据活过单局,都向本系统登记持久化归属,而不是本系统去追认每个系统。这条身份可验证:SaveManager 不引入任何玩法脚本、不碰数值表(ConfigDB)与随机服务(RngService),可以独立实例化、独立读写验证(冒烟独立段承载)。
存什么:表与档分家,局内系统资源不入档
第一条边界:数据表与存档分家。表是配方,所有玩家一致;存档是玩家进度,每人不同。存档只记进度事实(得了什么、解锁了什么),不把数值配方写进存档——数值改版后,旧存档仍按加载时的数据表解释。这条边界从"数值单一事实源在数据表"的纪律直接推出,细节以数据配置为准。
第二条边界是反面清单:局内资源不入存档。行动点、储备行动点、道具库存、生命、GP 全部单局清零——各归各的系统管,没有局内数值需要持久化。若未来某局内资源要开放跨局携带(如储备行动点跨战斗携带),须修订其池规则并向本系统登记持久化归属,禁止静默超域。
唯一豁免 = breakpoint 断点域(2026-09-29 迭代 8 卡①顺带修订):断点是冒险进行中的瞬态快照,不是跨局携带——里面的生命/GP/道具库存只在「续玩上局」时还原进局内,冒险收束(通关/失败/放弃)即整域清除,永远不会渗进永久进度。反面清单的管辖对象因此收窄为 meta 永久进度域(冒烟域界扫描顺带收窄到 meta 域);断点域自身的字段口径归冒险结构登记。
存档是单文件 JSON(user://save.json),字典键一律字符串。数据模型(现行 = v6;示例值全为零值默认态):
{
"version": 6,
"meta": {
"keystone_balance": 0,
"item_unlocks": {},
"level_clear_counts": {"1": 0, "2": 0, "3": 0},
"boss_first_kill": {},
"boss_kill_count": {},
"attunement_equipped": [],
"attune_refresh_level": 0,
"tree_nodes": {},
"fork_sides": {},
"tree_struct": 2,
"recent_summaries": [],
"first_adventure_done": false,
"adventure_total": 0,
"adventure_cleared": 0
},
"breakpoint": {}
}
| 域 | 存什么 | 内容 owner |
|---|---|---|
keystone_balance |
钥石余额——只增不减(消费除外) | 元进度 |
forge_levels |
属性锻造三档等级(生命上限/基础攻击/护甲等级) | 元进度 |
item_unlocks |
已解锁道具集合(道具工坊消费) | 元进度 |
level_clear_counts |
每关通关次数(键 = 关卡序号字符串) | 元进度 |
boss_first_kill |
Boss 首杀标记位(键 = 关卡序号字符串) | 元进度 |
boss_kill_count |
Boss 击杀总数(含重复——同调位增长谓词计数源,v4) | 元进度 |
attunement_equipped |
同调装配关系持久(冒险中途不可换装,v4) | 秘宝(承载语义归元进度) |
attune_refresh_level |
营地刷新升级(Y 源)等级(v4) | 秘宝(三选一刷新双源) |
tree_nodes |
天赋树已购等级(node_id→int,v5) | 天赋树 |
fork_sides |
Fork 组激活侧(fork_group→a/b,v5) | 天赋树 |
tree_struct |
树结构版本标记(=TREE_STRUCT_VERSION,迭代 21 T5;缺失 = 换代前结构,加载废弃重置) | 天赋树 |
recent_summaries |
最近冒险摘要副本,最新在前,最多 10 条 | 冒险结构 |
first_adventure_done |
首冒险完成标记(新档默认 false;旧档缺省按已完成兜底) | 元进度 |
adventure_total |
冒险总局数终身计数(开局即计、含放弃与失败,v6) | 生涯 |
adventure_cleared |
通关局数终身计数(三关全通才计 1,v6;秘宝条件解锁计数键) | 生涯 |
vision_used |
真视类工具累计使用次数终身计数(迭代 22 新增——信标网络解锁行为条件;v6 域后扩列补零、不升版本;写入唯一经 add_vision_used,冒险收束聚合入账;断点随件键=vision_counter_used,续玩回填缺省 0) |
秘宝 |
forge_levels |
已退役(迭代 21 T5:行族/访问器/域随攻防所有权切分删除——save_manager.gd:12-14/:399-401 白名单收窄;继任 = 锻造工坊路线等级域,见下行计划登记) |
|
forge_routes(已落地 v7——锻造工坊实装批 T5,迭代 23) |
{levels: 六路线等级(sword/spear/staff/light/heavy/ward), unlocked: 直购解锁路线 id 清单, weapon: 当前武器路线 id, armor: 当前甲路线 id};无特效域;旧档缺键补齐 = 初始持有态 | 锻造工坊 |
breakpoint |
冒险断点快照(停点粒度续玩域;空字典 = 无断点合法态——「继续上局」隐藏) | 冒险结构 |
v4/v5/v6 各域缺键由读取方按默认值补齐(空集合/0),旧档零迁移损失;收束自增域(通关次数/Boss 计数/终身计数/真视使用计数)的唯一入账点 = apply_adventure_summary(vision_used 走 add_vision_used 收束)。v6 域后扩列不升版先例:vision_used(迭代 22,2026-10-12——迭代 21 同款口径;断点随件 vision_counter_used 同批新增;旧断点随件键 bonus_aux_carry 已随替换子交互拆除退役,迭代 22 T5)。
消费面纪律:其他系统只经 SaveManager 的类型化访问器(读余额/加钥石/花钥石、读写树等级等)与域更新原语(apply_adventure_summary)操作存档,不碰内部数据结构。
读写时机
启动 ──▶ 读盘(load_from_disk:新档/损坏兜底/迁移链/高版本拒读,见下两节)
正式冒险收束(adventure_finished)──▶ apply_adventure_summary 入账 ──▶ 原子写盘
营地钥石消费(元进度实现批接线)──▶ 每笔变更后即时写盘
冒险停点(choices_presented 选路停点)──▶ 断点快照写盘(迭代 8 卡①)
冒险三结局收束(通关/失败/放弃)──▶ 清除断点 + 原子写盘(迭代 8 卡①/T3 退出口径)
写盘时机纪律:凡钥石发生变动的时刻都要落盘——正式冒险收束写一次,营地内每笔消费后即时写(元进度实现批接线)。这是"钥石不丢"判据的最强保证:任何时刻拔电源,玩家最多丢当前这一局的局内进程,不丢档。
断点写盘纪律(迭代 8 卡①):停点快照在每个选路停点(战斗胜利/兑付确认/入口呈现后的选路态)整体覆写——战斗进行中不写(棋盘中局不入档,续玩恢复至最近停点、当场战斗从头打)。因为快照总在每个停点即时落盘,「关游戏走人」在任何停点都不丢续玩进度,无需窗口关闭钩子。
写盘是原子操作:先写临时文件(<路径>.tmp),写完改名覆盖正式档——写一半断电不留烂档。
单局测试入口不写档:调试两层与 headless 单关测试路径(LevelRunner.start_level,仅 QA 工具使用)不接写盘——测试模式不污染生涯数据;只有正式冒险(AdventureRunner)收束才入账。
版本与迁移
存档带版本字段(version,当前 = 7;版本沿革:v2 = 断点域出生〔迭代 8 卡①〕→ v3 = 断点附随件扩列 + 摘要 keystone_drops〔迭代 10 奖励重构〕→ v4 = 同调批扩域〔boss_kill_count / attunement_equipped / attune_refresh_level,迭代 11 第①批〕→ v5 = 天赋树域〔tree_nodes / fork_sides,迭代 12〕→ v6 = 生涯终身计数域〔adventure_total / adventure_cleared,迭代 18 卡④〕→ v7 = 锻造工坊域〔forge_routes,迭代 23 T5——已落地:{levels 六路线等级 + unlocked 直购解锁清单 + weapon/armor 当前选中标记},无特效域;旧档缺键由 _normalize 按默认补齐 = 初始持有态(sword/light 在档、余四锁、选中=初始路线),v6→v7 空迁移 = 缺键补齐模式(v3→v4 先例);开发阶段禁迁移禁折算——契约 4〕;与 game/core/config_db.gd 的 SCHEMA_VERSION 同源)。三条走向:
| 情形 | 处置 |
|---|---|
| 版本相同 | 正常加载;旧档缺的新域按默认值补齐(既有值不动)——数据表加字段不弃档 |
| 版本更高(未来版本的档) | 拒读且禁写:不加载、后续写盘一律拒绝、内存以默认新档运行——保护未来版本的档不被当前版本覆写 |
| 版本更低(旧档) | 读档时逐级迁移:从旧版本起逐个调用注册的迁移函数升到当前版本;任一步失败按损坏兜底处理 |
迁移函数经 register_migration(旧版本号, 函数) 注册:函数把数据从「旧版本」改写成「旧版本 + 1」。生产迁移链现行六件(v2 起自注册):v1→v2 = 断点域补空;v2→v3 = 断点附随件与摘要 keystone_drops 补键(读取方 .get 兜底);v3→v4 = 同调三域补键;v4→v5 = 树域两键补齐(原挂接的「三体系迁移」体已随迭代 21 T5 攻防所有权切分删除——save_manager.gd:96-98、meta_progression.gd:294-295 实证;forge_levels 旧域同批清退,加载时按默认模型键集白名单收窄,save_manager.gd:399-401);v5→v6 = 生涯两终身计数域补零;v6→v7 = 锻造工坊域 forge_routes 缺键补齐(迭代 23 T5——无结构迁移体,版本号推进仅为 schema_version 对齐;解锁态派生不落档:初始持有〔表 initial_owned〕∨ 直购〔unlocked 清单〕∨ 练满前一档〔levels[prereq]≥cap〕)。任一步失败按损坏兜底处理。
损坏兜底
档案读不开(文件损坏、JSON 解析失败、结构出域、缺版本字段)时:原文件改名为 <路径>.corrupt.bak(已有同名备份则覆盖)留下现场,游戏用默认新档继续运行,并留下警告日志。两条底线:不静默丢档(坏档永远可人工找回)、不崩溃(坏档不挡玩)。
依赖与输出
本系统没有上游——基础层身份的另一半:它先于一切玩法系统存在,供所有人存取。已登记的下游与协作:
| 方向 | 系统 / 文档 | 接口要点 |
|---|---|---|
| 下游 | 元进度 | 钥石余额、锻造等级、通关次数、首杀标记的持久化承载方;对方以"钥石不丢"为验收,本系统以不破坏它为约束 |
| 下游 | 冒险结构 | 冒险摘要五件事副本入档(cap 10);快照与复盘的持久化承接 P1 后置 |
| 协作 | 数据配置 | 表与档分家:数值口径永远以加载时的表为准 |
P0 最小闭环
MVP 不要求本系统的闭环(不做清单把单局循环排前)。本批已落地的最小集:存档域首批内容(元进度四件 + 最近冒险摘要)能写入、读回、跨进程存活;版本与损坏策略就位;正式冒险收束自动入账。元进度消费面(花钥石、买强化、营地界面)归元进度实现批——承载已就绪,内容方接线即可用。
P1 / P2 后置
| 优先级 | 内容 | 后置原因 |
|---|---|---|
已兑现(2026-09-29 迭代 8 卡①:断点域承载 + rng「主种子+计数器」快照契约回填面 restore_adventure) |
||
| P2 | 多存档槽位 | 单档 MVP(2026-09-21 裁决);多档等有真实需求再立项 |
调参表
本系统没有玩法调参数值。两个结构常量:存档版本号(=6,与 game/core/config_db.gd 的 SCHEMA_VERSION 同源)、最近摘要保留条数(10)。二者是结构契约不是平衡值,改动属于版本迁移事件,不走调参批。
验收标准
- 往返一致:全部域写入非默认值 → 写盘 → 新实例读回,逐域逐值相等(冒烟承载)。
- 钥石不丢:写入、读回与迁移禁止破坏元进度的钥石只增不减判据;余额不足的消费被拒绝且无副作用(冒烟承载)。
- 高版本拒读禁写:高版本档加载被拒、原文件字节不动、后续写盘被拒且文件仍字节不动(冒烟承载)。
- 改版不弃档:旧版本档经迁移链读回;低版本档缺新域按默认补齐,既有值保留(冒烟承载)。
- 断点域契约(迭代 8 卡①):七字段拍板面写读往返逐位一致;旧档无断点域迁移为合法空态;三结局清除(冒烟+单测承载)。
- 域界干净:meta 永久进度域不含局内系统资源键(行动点/储备行动点/道具库存/生命/GP)——断点域按豁免条款单列(冒烟承载,2026-09-29 卡①修订)。
- 零玩法依赖:SaveManager 不引入任何玩法脚本、不碰数值表与随机服务,可独立实例化验证(冒烟独立段承载)。
待办
本系统待办条目已统一收编至 待办清单,本节不再逐条登记。