18 KiB
存档序列化
一句话:存档契约与版本迁移 状态:已定稿;现行为 v1.3
设计目的
本系统管的是"关掉游戏之后,玩家的东西还在不在"。序列化(本文含义:把游戏运行中的数据写成可存盘的档案,下次启动再原样读回来)与版本迁移(本文含义:游戏更新后,旧版本写下的存档要能被新版本继续读懂,必要时把旧结构改写成新结构)合起来,就是"存档契约"这门生意的全部。
这件事是核心循环里"存档在变厚"那条故事线的底层承载:钥石攒进存档、倒下也不丢——写在纸面上就是本系统的职责。本系统在切片里的落地面是存档管理器:Core 层,零界面依赖、零玩法依赖。本文描述的就是这份契约:存什么、何时读写、版本怎么走、坏了怎么办。
系统范围
边界这样划:本系统只管存档的技术契约面——什么算一份完整的档案、何时读写、旧档怎么跟着版本走。至于存进去的内容归谁定(钥石算什么、摘要记什么),那是各内容系统的口径,本文照单承载,不替任何人定义进度。
负责:
| 负责 | 说明 |
|---|---|
| 存档契约 | 3 存档位 JSON 存档的域划分与读写约定(域清单见核心规则;槽位承载见下「三存档位」段) |
| 版本迁移 | 版本字段 + 读档时链式迁移 + 高版本拒读禁写 |
| 读写时机 | 启动读盘;正式冒险收束写盘;原子写盘原语 |
| 元进度的持久化承载 | 归档登记的职责口径:"元进度持久化框架"——钥石与解锁进度由本系统落盘 |
| 最近冒险摘要的持久化 | 冒险摘要五件事副本入档(cap 10),重开对照跨进程成立 |
| 冒险断点快照的持久化承载 | 停点粒度续玩快照(breakpoint 域)写入/读回/清除/迁移——内容口径归冒险结构 |
不负责:
| 不负责 | 归谁 |
|---|---|
| 存进去的内容口径(钥石怎么得、能换什么) | 元进度——本系统只承载,不定义 |
| 冒险摘要记哪几件事 | 冒险结构——本系统只消费其摘要五件事 |
| 冒险断点快照记哪些字段、何时算一个合法停点 | 冒险结构——本系统只承载 breakpoint 域的写入/读回/清除 |
| Boss 击杀的钥石金额计算(首杀全量 / 重复杀小额) | 元进度——本系统只记域内事实,不算账 |
| 数值配方本体(所有玩家一致的表) | 数据配置——表是配方,存档是玩家进度,两者分离(见核心规则) |
| 局内数值的持久化 | 无——局内系统资源全部单局清零,没有局内数值需要入档(见核心规则的反面清单) |
| 系统级设置(音量/画面等) | 不入档——设置与存档两分(2026-09-19 用户拍板):设置独立存储、一安装一份,存档只存生涯数据;设置域归设置与本地化所有 |
在核心循环中的位置
游玩中攒下的进度事实(钥石到手 / 冒险结算)──▶ 存档序列化写盘 ──(下次启动读回)──▶ "存档在变厚"被兑现
存档序列化不占核心循环里的一段,它贴在"死亡重开"出口的外侧:局内胜负归零时,它保证玩家攒下的东西不跟着归零。核心循环把"存档在变厚"讲成玩家重开的理由之一,本系统就是那条理由的底层承载。
核心规则
基础层身份:无上游依赖的承载方
存档系统不依赖任何玩法系统就能工作:它是被动的承载方,任何系统要把数据活过单局,都向本系统登记持久化归属,而不是本系统去追认每个系统。这条身份可验证:存档系统不引入任何玩法脚本、不碰数值表与随机服务,可以独立实例化、独立读写验证(冒烟独立段承载)。
存什么:表与档分家,局内系统资源不入档
第一条边界:数据表与存档分家。表是配方,所有玩家一致;存档是玩家进度,每人不同。存档只记进度事实(得了什么、解锁了什么),不把数值配方写进存档——数值改版后,旧存档仍按加载时的数据表解释。这条边界从"数值单一事实源在数据表"的纪律直接推出,细节以数据配置为准。
第二条边界是反面清单:局内资源不入存档。行动点、储备行动点、道具库存、生命、GP 全部单局清零——各归各的系统管,没有局内数值需要持久化。若未来某局内资源要开放跨局携带(如储备行动点跨战斗携带),须修订其池规则并向本系统登记持久化归属,禁止静默超域。
唯一豁免 = breakpoint 断点域:断点是冒险进行中的瞬态快照,不是跨局携带——里面的生命/GP/道具库存只在「续玩上局」时还原进局内,冒险收束(通关/失败/放弃)即整域清除,永远不会渗进永久进度。反面清单的管辖对象因此收窄为 meta 永久进度域(冒烟域界扫描收窄到 meta 域);断点域自身的字段口径归冒险结构登记。断点顶层键(28 键)的登记单源=冒险应用层的断点键表(键→回退策略;现行契约见架构文档「冒险域拆分」节);写读已换代为各件自交片段——主角账/停点件/关卡件各自写读自己的键(缺键回退策略随领域知识归各件),冒险应用层是唯一拼盘落盘口(收齐各件片段拼成顶层快照后写盘,写触发点不变)。关卡完成停点的同调未用行留存:aux_items 键在此停点形态含同调未用行(「同调行始终留在 treasure_inventory」与「键值形态逐位不变」在关卡完成停点互斥,采行留存;玩家可见 C 区投影零变化,24 停点充能读数逐位一致)。
三存档位:存档是 3 个独立的 JSON 文件(三个独立存档文件,槽文件名只经存档系统单点派生,槽 1 禁复用旧单档名),字典键一律字符串。写盘目标=当前激活槽的文件;启动(boot)不读任何槽文件——启动态=无激活态,不读写;激活态由选档界面的「新档」或「进档」建立。数据模型(现行 = v7;示例值全为零值默认态):
{
"version": 7,
"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 |
树结构版本标记(缺失 = 换代前结构,加载废弃重置) | 天赋树 |
recent_summaries |
最近冒险摘要副本,最新在前,最多 10 条 | 冒险结构 |
first_adventure_done |
首冒险完成标记(新档默认 false;旧档缺省按已完成兜底) | 元进度 |
adventure_total |
冒险总局数终身计数(开局即计、含放弃与失败,v6) | 生涯 |
adventure_cleared |
冒险成功局数终身计数(三关全通才计 1,v6;秘宝条件解锁计数键) | 生涯 |
vision_used |
真视类工具累计使用次数终身计数(信标网络解锁行为条件;v6 域后扩列补零、不升版本;写入唯一经真视计数入口,冒险收束聚合入账;断点随件键=vision_counter_used,续玩回填缺省 0) |
秘宝 |
forge_levels |
已退役(继任 = 锻造路线等级域) | |
forge_routes(已落地 v7) |
{levels: 六路线等级(sword/spear/staff/light/heavy/ward), unlocked: 直购解锁路线 id 清单, weapon: 当前武器路线 id, armor: 当前甲路线 id};无特效域;旧档缺键补齐 = 初始持有态 | 锻造工坊 |
breakpoint |
冒险断点快照(停点粒度续玩域;空字典 = 无断点合法态——「继续上局」隐藏) | 冒险结构 |
v4/v5/v6 各域缺键由读取方按默认值补齐(空集合/0),旧档零迁移损失;收束自增域(通关次数/Boss 计数/终身计数/真视使用计数)的唯一入账点 = 冒险摘要入账(vision_used 走真视计数入口收束)。
消费面纪律:其他系统只经存档系统的类型化访问器(读余额/加钥石/花钥石、读写树等级等)与域更新原语(冒险摘要入账)操作存档,不碰内部数据结构。三条槽位纪律:一、无激活态下一切写盘路径拒绝并返回 false(boot 后未进档/未新档即结算或购买=不落盘,激活后照常);二、槽文件名单点派生——消费方禁各自拼槽文件名,测试与探针的隔离走注入口(槽模板注入/摘要窥视);三、UI 读别槽摘要只走窥视 API(不激活、不写内存、零盘面副作用),返回结构 = 可读标记 + 三项摘要值(钥石/通关数/总局数)——可读标记 = 槽文件可读(空槽=零值合法态可读;损坏/高版本/缺版本字段=不可读标记);槽主文件在盘与否不作摘要项、只供操作集二分判定(零 UI 语义),三项摘要值只在可读时有意义。
读写时机
启动 ──▶ 不读盘(boot 无激活态,不读写任何槽;只做一次旧单档迁移,见「版本与迁移」节)
选档界面「新档」/「进档」──▶ 建立激活态(读盘=该槽文件)
正式冒险收束 ──▶ 冒险摘要入账 ──▶ 原子写盘(落当前激活槽)
营地钥石消费 ──▶ 每笔变更后即时写盘(落当前激活槽)
冒险停点(选路停点)──▶ 断点快照写盘
冒险三结局收束(冒险成功/失败/放弃)──▶ 清除断点 + 原子写盘
写盘时机纪律:凡钥石发生变动的时刻都要落盘——正式冒险收束写一次,营地内每笔消费后即时写。这是"钥石不丢"判据的最强保证:任何时刻拔电源,玩家最多丢当前这一局的局内进程,不丢档。
断点写盘纪律:停点快照在每个选路停点(战斗胜利/兑付确认/入口呈现后的选路态)整体覆写——战斗进行中不写(棋盘中局不入档,续玩恢复至最近停点、当场战斗从头打)。因为快照总在每个停点即时落盘,「关游戏走人」在任何停点都不丢续玩进度,无需窗口关闭钩子。
写盘是原子操作:先写同目录临时文件,写完改名覆盖正式档——写一半断电不留烂档。
单局测试入口不写档:调试两层与 headless 单关测试路径(仅 QA 工具使用)不接写盘——测试模式不污染生涯数据;只有正式冒险收束才入账。
版本与迁移
存档带版本字段(version,当前 = 7;版本沿革:v2 = 断点域出生 → v3 = 断点附随件扩列 + 摘要 keystone_drops → v4 = 同调扩域〔boss_kill_count / attunement_equipped / attune_refresh_level〕→ v5 = 天赋树域〔tree_nodes / fork_sides〕→ v6 = 生涯终身计数域〔adventure_total / adventure_cleared〕→ v7 = 锻造工坊域〔forge_routes:{levels 六路线等级 + unlocked 直购解锁清单 + weapon/armor 当前选中标记},无特效域;旧档缺键按默认补齐 = 初始持有态(sword/light 在档、余四锁、选中=初始路线),v6→v7 空迁移 = 缺键补齐模式(v3→v4 先例);开发阶段禁迁移禁折算——契约 4〕)。三条走向:
| 情形 | 处置 |
|---|---|
| 版本相同 | 正常加载;旧档缺的新域按默认值补齐(既有值不动)——数据表加字段不弃档 |
| 版本更高(未来版本的档) | 拒读且禁写:不加载、后续写盘一律拒绝、内存以默认新档运行——保护未来版本的档不被当前版本覆写 |
| 版本更低(旧档) | 读档时逐级迁移:从旧版本起逐个调用注册的迁移函数升到当前版本;任一步失败按损坏兜底处理 |
迁移函数按旧版本号逐一注册:函数把数据从「旧版本」改写成「旧版本 + 1」。生产迁移链现行六件(v2 起自注册):v1→v2 = 断点域补空;v2→v3 = 断点附随件与摘要 keystone_drops 补键(读取方 .get 兜底);v3→v4 = 同调三域补键;v4→v5 = 树域两键补齐(forge_levels 旧域清退,加载时按默认模型键集白名单收窄);v5→v6 = 生涯两终身计数域补零;v6→v7 = 锻造工坊域 forge_routes 缺键补齐(无结构迁移体,版本号推进仅随表结构变化;解锁态派生不落档:初始持有〔表 initial_owned〕∨ 直购〔unlocked 清单〕∨ 练满前一档〔levels[prereq]≥cap〕)。任一步失败按损坏兜底处理。
旧单档一次性迁移:本系统从单档制换代为 3 存档位制时,盘上残留的旧单档文件在启动时被处理——文件字节级原样搬运进槽 1(零解析零改造:损坏或高版本的旧档也原样迁入,之后由该槽进档时的损坏兜底/高版本拒读接管,不静默丢数据),源文件改名留痕不删;槽 1 文件已存在则不重迁(幂等,二次启动零操作);迁移失败(拷贝或改名失败)源文件保留、下次启动重试。留痕与结果记在启动日志(开发者向,非玩家面)。
损坏兜底
档案读不开(文件损坏、JSON 解析失败、结构出域、缺版本字段)时:原文件改名留一份坏档备份(已有同名备份则覆盖)留下现场,游戏用默认新档继续运行,并留下警告日志。两条底线:不静默丢档(坏档永远可人工找回)、不崩溃(坏档不挡玩)。
依赖与输出
本系统没有上游——基础层身份的另一半:它先于一切玩法系统存在,供所有人存取。已登记的下游与协作:
| 方向 | 系统 / 文档 | 接口要点 |
|---|---|---|
| 下游 | 元进度 | 钥石余额、锻造等级、关卡完成次数、首杀标记的持久化承载方;对方以"钥石不丢"为验收,本系统以不破坏它为约束 |
| 下游 | 冒险结构 | 冒险摘要五件事副本入档(cap 10);快照与复盘的持久化承接 |
| 协作 | 数据配置 | 表与档分家:数值口径永远以加载时的表为准 |
P0 最小闭环
MVP 不要求本系统的闭环(不做清单把单局循环排前)。本批已落地的最小集:存档域首批内容(元进度四件 + 最近冒险摘要)能写入、读回、跨进程存活;版本与损坏策略就位;正式冒险收束自动入账。元进度消费面(花钥石、买强化、营地界面)归元进度实现批——承载已就绪,内容方接线即可用。
P1 / P2 后置
| 优先级 | 内容 | 后置原因 |
|---|---|---|
| 已兑现(断点域承载 + rng「主种子+计数器」快照契约回填面) | ||
| 已实现(3 存档位三文件制,口径见「三存档位」段) |
调参表
本系统没有玩法调参数值。两个结构常量:存档版本号(当前 7)、最近摘要保留条数(10)。二者是结构契约不是平衡值,改动属于版本迁移事件,不走调参批。
验收标准
- 往返一致:全部域写入非默认值 → 写盘 → 新实例读回,逐域逐值相等(冒烟承载)。
- 钥石不丢:写入、读回与迁移禁止破坏元进度的钥石只增不减判据;余额不足的消费被拒绝且无副作用(冒烟承载)。
- 高版本拒读禁写:高版本档加载被拒、原文件字节不动、后续写盘被拒且文件仍字节不动(冒烟承载)。
- 改版不弃档:旧版本档经迁移链读回;低版本档缺新域按默认补齐,既有值保留(冒烟承载)。
- 断点域契约:七字段拍板面写读往返逐位一致;旧档无断点域迁移为合法空态;三结局清除(冒烟+单测承载)。
- 域界干净:meta 永久进度域不含局内系统资源键(行动点/储备行动点/道具库存/生命/GP)——断点域按豁免条款单列(冒烟承载)。
- 零玩法依赖:存档系统不引入任何玩法脚本、不碰数值表与随机服务,可独立实例化验证(冒烟独立段承载)。
待办
本系统待办条目已统一收编至 待办清单,本节不再逐条登记。