Files
gplan-2/design/gdd/systems/表现与支撑/存档序列化.md
T
DKChan 7aee5eb71a docs(流程文档迁出GDD批): 待办清单与体验验证清单移到仓库根并去掉编号前缀
两文件是流水线流程件而非游戏描述件,此前住在 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/ 两处注释
2026-10-21 00:09:40 +08:00

17 KiB
Raw Blame History

存档序列化

一句话:存档契约与版本迁移 状态:已定稿(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 后置

优先级 内容 后置原因
P1 快照与复盘的持久化承接 已兑现(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 不引入任何玩法脚本、不碰数值表与随机服务,可独立实例化验证(冒烟独立段承载)。

待办

本系统待办条目已统一收编至 待办清单,本节不再逐条登记。