Files
gplan-2/design/gdd/systems/表现与支撑/存档序列化.md
T

18 KiB
Raw Blame History

存档序列化

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

优先级 内容 后置原因
P1 快照与复盘的持久化承接 已兑现(断点域承载 + rng「主种子+计数器」快照契约回填面)
P2 多存档槽位 已实现(3 存档位三文件制,口径见「三存档位」段)

调参表

本系统没有玩法调参数值。两个结构常量:存档版本号(当前 7)、最近摘要保留条数(10)。二者是结构契约不是平衡值,改动属于版本迁移事件,不走调参批。

验收标准

  • 往返一致:全部域写入非默认值 → 写盘 → 新实例读回,逐域逐值相等(冒烟承载)。
  • 钥石不丢:写入、读回与迁移禁止破坏元进度的钥石只增不减判据;余额不足的消费被拒绝且无副作用(冒烟承载)。
  • 高版本拒读禁写:高版本档加载被拒、原文件字节不动、后续写盘被拒且文件仍字节不动(冒烟承载)。
  • 改版不弃档:旧版本档经迁移链读回;低版本档缺新域按默认补齐,既有值保留(冒烟承载)。
  • 断点域契约:七字段拍板面写读往返逐位一致;旧档无断点域迁移为合法空态;三结局清除(冒烟+单测承载)。
  • 域界干净:meta 永久进度域不含局内系统资源键(行动点/储备行动点/道具库存/生命/GP)——断点域按豁免条款单列(冒烟承载)。
  • 零玩法依赖:存档系统不引入任何玩法脚本、不碰数值表与随机服务,可独立实例化验证(冒烟独立段承载)。

待办

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