④T1: 存档 v6 生涯终身计数域——SAVE_FORMAT_VERSION 5→6(版本史注释随批换代)+meta 两新域 adventure_total〔总局数,开局即计含放弃与失败〕/adventure_cleared〔通关局数,三关全通才计 1——判定字段 summary.cleared 现成直用〕+迁移函数 _migrate_v5_v6(沿现役迁移链 _init 自注册;旧档缺键 _normalize 按默认补零、只增不改=幂等,v3→v4 先例)+只读访问器 adventure_total()/adventure_cleared()+写入单口=apply_adventure_summary 收束口头部自增(三结局同经本口恰计一次,禁第二写入点——契约 2;迁移链陈旧注释 v1→v3/v1→v6 顺手校正)+域清单 owner 注记(生涯.md)+registry save_file 条目 v4→v6 换代(v5 批漏账补齐)+冒烟随批:硬断言「写盘 version==5」两处改 6(迁移链段+v1 旧档段)+①往返段收束自增恰一/往返相等两断言+②新档段缺省全零断言+⑬新增段(v5 旧档升 v6 缺省补零且既有余额原样/升档幂等写盘再载零漂移/通关局 cleared+1 夹具)——冒烟 821/0(基线 814+7)

This commit is contained in:
DKChan committed 2026-10-07 08:35:47 +08:00
1 parent cf2aa03f87
commit 669c1c7d01
3 files changed
+75 -12

No files matched your search

+2 -2
View File
@@ -69,8 +69,8 @@ entities:
attributes: attributes:
path: user://save.json(单档 MVP——多存档槽位 P2 保留) path: user://save.json(单档 MVP——多存档槽位 P2 保留)
format: JSON 单档(人可读可调试;字典键一律 String) format: JSON 单档(人可读可调试;字典键一律 String)
version: SAVE_FORMAT_VERSION=4(registry schema_version 同源;v4=同调批扩域 2026-10-03 迭代 11 第①批——meta 三新域 boss_kill_count/attunement_equipped/attune_refresh_level + 断点附随件 attunement_charges,旧档缺键由 _normalize 按默认补齐;v3=断点附随件扩列+摘要 keystone_drops 2026-10-01 迭代 10;v2=断点域出生 2026-09-29 迭代 8 卡①;生产迁移链 v1→v4 链式补空;高版本拒读 + 禁写盘 + 内存默认新档运行;低版本读档时链式迁移 register_migration,任一步失败按损坏兜底) version: SAVE_FORMAT_VERSION=6(registry schema_version 同源;v6=生涯终身计数域 2026-10-07 迭代 18 卡④——meta 两新域 adventure_total〔冒险总局数,开局即计含放弃与失败〕/adventure_cleared〔通关局数,三关全通才计 1——判定字段 summary.cleared〕,旧档缺键由 _normalize 按默认补零,收束自增唯一入账=apply_adventure_summary;v5=天赋树域 2026-10-04 迭代 12 T9——meta 三新域 tree_nodes/fork_sides/migration_note;v4=同调批扩域 2026-10-03 迭代 11 第①批——meta 三新域 boss_kill_count/attunement_equipped/attune_refresh_level + 断点附随件 attunement_charges,旧档缺键由 _normalize 按默认补齐;v3=断点附随件扩列+摘要 keystone_drops 2026-10-01 迭代 10;v2=断点域出生 2026-09-29 迭代 8 卡①;生产迁移链 v1→v6 链式补空;高版本拒读 + 禁写盘 + 内存默认新档运行;低版本读档时链式迁移 register_migration,任一步失败按损坏兜底)
domains: meta 域九件——keystone_balance(元进度)/ forge_levels 三档(元进度)/ item_unlocks(元进度)/ level_clear_counts(元进度)/ boss_first_kill(元进度)/ boss_kill_count(秘宝——同调位里程碑计数源,Boss 恒唯一终点故收束入账=levels_cleared)/ attunement_equipped(秘宝——装配关系有序数组,局外进度事实非局内道具库存,装配序即呈现序)/ attune_refresh_level(秘宝——Y 刷新升级购买等级)/ recent_summaries cap 10(冒险结构,元素附 boss_drops 副本);+ breakpoint 域(断点快照:七字段拍板面+附随件〔主种子/取数计数器/钥石累计/已到节点序/道具库存/bonus_* 附随件/attunement_charges 充能余量——迭代 11 第①批 T6〕,空字典=无断点合法态,三结局清除) domains: meta 域十一件——keystone_balance(元进度)/ forge_levels 三档(元进度)/ item_unlocks(元进度)/ level_clear_counts(元进度)/ boss_first_kill(元进度)/ boss_kill_count(秘宝——同调位里程碑计数源,Boss 恒唯一终点故收束入账=levels_cleared)/ attunement_equipped(秘宝——装配关系有序数组,局外进度事实非局内道具库存,装配序即呈现序)/ attune_refresh_level(秘宝——Y 刷新升级购买等级)/ tree_nodes+fork_sides(天赋树——迭代 12 T9 树域)/ adventure_total+adventure_cleared(生涯——终身计数域,迭代 18 卡④,写入唯一经 apply_adventure_summary)/ recent_summaries cap 10(冒险结构,元素附 boss_drops 副本);+ breakpoint 域(断点快照:七字段拍板面+附随件〔主种子/取数计数器/钥石累计/已到节点序/道具库存/bonus_* 附随件/attunement_charges 充能余量——迭代 11 第①批 T6〕,空字典=无断点合法态,三结局清除)
write_timing: 启动读盘;正式冒险收束(adventure_finished → apply_adventure_summary)写盘;营地每笔钥石变更后即时写(元进度实现批接线);冒险选路停点断点快照写盘 + 三结局清断点写盘(迭代 8 卡①);调试两层与单关测试路径(LevelRunner.start_level,仅 QA 工具)不写档 write_timing: 启动读盘;正式冒险收束(adventure_finished → apply_adventure_summary)写盘;营地每笔钥石变更后即时写(元进度实现批接线);冒险选路停点断点快照写盘 + 三结局清断点写盘(迭代 8 卡①);调试两层与单关测试路径(LevelRunner.start_level,仅 QA 工具)不写档
atomic_write: tmp + rename 原子写(写一半断电不留烂档——钥石不丢判据承载) atomic_write: tmp + rename 原子写(写一半断电不留烂档——钥石不丢判据承载)
corruption_policy: 坏档改名 <path>.corrupt.bak(已存在覆盖)+ 默认新档继续 + 警告留痕(不静默丢档不 crash) corruption_policy: 坏档改名 <path>.corrupt.bak(已存在覆盖)+ 默认新档继续 + 警告留痕(不静默丢档不 crash)
+37 -4
View File
@@ -17,6 +17,9 @@
# breakpoint.attunement_charges(主动件充能余量)——每冒险重置语义的断点瞬态附随件,内容装配归 # breakpoint.attunement_charges(主动件充能余量)——每冒险重置语义的断点瞬态附随件,内容装配归
# AdventureRunner(随 T6 接线入断点),本类照单承载;契约 2 域界注记同断点域豁免先例 # AdventureRunner(随 T6 接线入断点),本类照单承载;契约 2 域界注记同断点域豁免先例
# recent_summaries(冒险摘要五件事副本)—— 内容 owner = 冒险结构.md(摘要五件事口径) # recent_summaries(冒险摘要五件事副本)—— 内容 owner = 冒险结构.md(摘要五件事口径)
# adventure_total / adventure_cleared(生涯终身计数域)—— 内容 owner = 生涯.md(跨局汇总页
# 数据源:总局数开局即计〔三结局同经收束恰计一次,含放弃与失败〕/通关局数三关全通才计 1
# ——判定字段 summary.cleared;写入唯一经 apply_adventure_summary,迭代 18 卡④)
# breakpoint(冒险断点快照——停点粒度续玩域)—— 内容 owner = 冒险结构.md(七字段拍板面 # breakpoint(冒险断点快照——停点粒度续玩域)—— 内容 owner = 冒险结构.md(七字段拍板面
# +恢复必需附随件:主种子/取数计数器/钥石累计/已到节点序/道具库存);契约 2 域界反面清单 # +恢复必需附随件:主种子/取数计数器/钥石累计/已到节点序/道具库存);契约 2 域界反面清单
# 只约束 meta 永久进度域——断点域=冒险进行中瞬态快照,生命/GP/道具库存按拍板面入域 # 只约束 meta 永久进度域——断点域=冒险进行中瞬态快照,生命/GP/道具库存按拍板面入域
@@ -36,7 +39,7 @@
class_name SaveManager class_name SaveManager
extends RefCounted extends RefCounted
const SAVE_FORMAT_VERSION := 5 # registry schema_version:v5=天赋树域(迭代 12 T9——meta 三新域 tree_nodes〔node_id→已购等级〕/fork_sides〔fork_group→a|b〕/migration_note〔三体系迁移补偿注记〕——M-4 迭代 12 批次二;任务书「v3→v4」系同调批未落时行文,v4 已被同调批占用顺延 v5 披露);v4=同调批扩域(meta 三新域 boss_kill_count/attunement_equipped/attune_refresh_level——M-4 迭代 11 第①批 T2;断点充能余量随 T6 接线,旧档缺键 = 合法空态);v3=断点附随件扩列+摘要 keystone_drops(M-3 迭代 10 奖励重构);v2=断点域出生(M-3 迭代 8 卡①);v1=首版;高版本拒读 owner=存档序列化 const SAVE_FORMAT_VERSION := 6 # registry schema_version:v6=生涯终身计数域(迭代 18 卡④——meta 两新域 adventure_total〔冒险总局数,开局即计含放弃与失败〕/adventure_cleared〔通关局数,三关全通才计 1——判定字段 summary.cleared〕,旧档缺键由 _normalize 按默认补零,收束自增唯一入账=apply_adventure_summary);v5=天赋树域(迭代 12 T9——meta 三新域 tree_nodes〔node_id→已购等级〕/fork_sides〔fork_group→a|b〕/migration_note〔三体系迁移补偿注记〕——M-4 迭代 12 批次二;任务书「v3→v4」系同调批未落时行文,v4 已被同调批占用顺延 v5 披露);v4=同调批扩域(meta 三新域 boss_kill_count/attunement_equipped/attune_refresh_level——M-4 迭代 11 第①批 T2;断点充能余量随 T6 接线,旧档缺键 = 合法空态);v3=断点附随件扩列+摘要 keystone_drops(M-3 迭代 10 奖励重构);v2=断点域出生(M-3 迭代 8 卡①);v1=首版;高版本拒读 owner=存档序列化
const SUMMARY_CAP := 10 # 最近冒险摘要保留条数(最新在前) const SUMMARY_CAP := 10 # 最近冒险摘要保留条数(最新在前)
const FORGE_ATTRS := ["max_hp", "atk", "armor_class"] # 属性锻造三档合法键(元进度.md 属性锻造柜台) const FORGE_ATTRS := ["max_hp", "atk", "armor_class"] # 属性锻造三档合法键(元进度.md 属性锻造柜台)
@@ -44,7 +47,7 @@ const FORGE_ATTRS := ["max_hp", "atk", "armor_class"] # 属性锻造三档合
var _path: String # 存档文件路径(默认 user://save.json;冒烟注入隔离路径) var _path: String # 存档文件路径(默认 user://save.json;冒烟注入隔离路径)
var _read_only := false # 高版本拒读置位——write_to_disk 一律拒绝(契约 4 禁写) var _read_only := false # 高版本拒读置位——write_to_disk 一律拒绝(契约 4 禁写)
var _data: Dictionary[String, Variant] = {} # 内存数据模型(_init 即内置默认;load 后为档案内容) var _data: Dictionary[String, Variant] = {} # 内存数据模型(_init 即内置默认;load 后为档案内容)
var _migrations: Dictionary[int, Callable] = {} # from_version → Callable(data)->data(升一版;v1→v3 生产迁移链现行——_init 自注册) var _migrations: Dictionary[int, Callable] = {} # from_version → Callable(data)->data(升一版;v1→v6 生产迁移链现行——_init 自注册)
func _init(path: String = "user://save.json") -> void: func _init(path: String = "user://save.json") -> void:
@@ -55,6 +58,7 @@ func _init(path: String = "user://save.json") -> void:
register_migration(2, _migrate_v2_v3) register_migration(2, _migrate_v2_v3)
register_migration(3, _migrate_v3_v4) register_migration(3, _migrate_v3_v4)
register_migration(4, _migrate_v4_v5) register_migration(4, _migrate_v4_v5)
register_migration(5, _migrate_v5_v6)
# v1→v2:断点域出生(旧档无断点 = 合法空态——「继续上局」隐藏,非损坏非债务) # v1→v2:断点域出生(旧档无断点 = 合法空态——「继续上局」隐藏,非损坏非债务)
@@ -97,6 +101,14 @@ func _migrate_v4_v5(data: Dictionary) -> Dictionary:
return data return data
# v5→v6:生涯终身计数域(迭代 18 卡④——meta 两新域 adventure_total/adventure_cleared,旧档缺键
# 由 _normalize 按默认补零,无结构迁移体;版本号推进仅为 schema_version 对齐——v3→v4 先例。
# 幂等性:重复迁移只覆写 version,既有值零改动;加载后 _normalize 补齐缺省——旧档可载可玩)
func _migrate_v5_v6(data: Dictionary) -> Dictionary:
data["version"] = SAVE_FORMAT_VERSION
return data
# ---- 默认数据模型(新档合法态——全零;白板存档 = 完整可玩〔元进度 单局循环零依赖〕)---- # ---- 默认数据模型(新档合法态——全零;白板存档 = 完整可玩〔元进度 单局循环零依赖〕)----
func _default_data() -> Dictionary[String, Variant]: func _default_data() -> Dictionary[String, Variant]:
@@ -115,6 +127,8 @@ func _default_data() -> Dictionary[String, Variant]:
"fork_sides": {}, # A/B 激活侧(fork_group→"a"|"b"——营地免费切换承载) "fork_sides": {}, # A/B 激活侧(fork_group→"a"|"b"——营地免费切换承载)
"recent_summaries": [], "recent_summaries": [],
"first_adventure_done": false, "first_adventure_done": false,
"adventure_total": 0, # 生涯终身计数·冒险总局数(迭代 18 卡④——开局即计含放弃与失败,收束唯一入账)
"adventure_cleared": 0, # 生涯终身计数·通关局数(三关全通才计 1——判定字段 summary.cleared)
}, },
"breakpoint": {}, # 冒险断点快照(空 = 无断点合法态——「继续上局」隐藏;迭代 8 卡①) "breakpoint": {}, # 冒险断点快照(空 = 无断点合法态——「继续上局」隐藏;迭代 8 卡①)
} }
@@ -191,7 +205,7 @@ func write_to_disk() -> bool:
# 迁移注册口:from_version → from_version+1 的升级函数(读档时链式调用)。fn 契约: # 迁移注册口:from_version → from_version+1 的升级函数(读档时链式调用)。fn 契约:
# Dictionary → Dictionary,须写回 version = from_version + 1。生产迁移链现行(v1→v3,_init 自注册), # Dictionary → Dictionary,须写回 version = from_version + 1。生产迁移链现行(v1→v6,_init 自注册),
# 冒烟经本口验证链式升级。 # 冒烟经本口验证链式升级。
func register_migration(from_version: int, fn: Callable) -> void: func register_migration(from_version: int, fn: Callable) -> void:
_migrations[from_version] = fn _migrations[from_version] = fn
@@ -251,6 +265,20 @@ func boss_kill_count() -> int:
return int(_data["meta"].get("boss_kill_count", 0)) return int(_data["meta"].get("boss_kill_count", 0))
# ---- 生涯终身计数域(迭代 18 卡④——跨局汇总页数据源;只读面经访问器,
# 写入唯一经 apply_adventure_summary 收束口,禁第二写入点)----
# 冒险总局数(口径=开局即计:三结局 completed/failed/abandoned 同经收束恰计一次,含放弃与失败)
func adventure_total() -> int:
return int(_data["meta"].get("adventure_total", 0))
# 通关局数(口径=三关全通才计 1——判定字段 summary.cleared,adventure_runner「completed」
# 唯一出口=末关通关)
func adventure_cleared() -> int:
return int(_data["meta"].get("adventure_cleared", 0))
# ---- 天赋树域(迭代 12 T9——MetaProgression 树段读写面)---- # ---- 天赋树域(迭代 12 T9——MetaProgression 树段读写面)----
func tree_nodes() -> Dictionary: func tree_nodes() -> Dictionary:
@@ -321,8 +349,13 @@ func recent_summaries() -> Array:
# 钥石入账(summary.keystone_total——迭代 10 起逐杀掉落已在冒险内计入,boss_drops 参数降级为 # 钥石入账(summary.keystone_total——迭代 10 起逐杀掉落已在冒险内计入,boss_drops 参数降级为
# 摘要归档副本不重复入账)+ 逐通关关次 clear_count +1 与 Boss 首杀标记 + Boss 击杀总数累计 # 摘要归档副本不重复入账)+ 逐通关关次 clear_count +1 与 Boss 首杀标记 + Boss 击杀总数累计
# (迭代 11 第①批 T2——同调位里程碑计数源)+ 摘要头部插入(cap 10,最新在前,元素附 # (迭代 11 第①批 T2——同调位里程碑计数源)+ 摘要头部插入(cap 10,最新在前,元素附
# keystone_drops 副本——复盘可归因)。 # keystone_drops 副本——复盘可归因)+ 生涯终身计数自增(迭代 18 卡④——总局数开局即计:
# 三结局同经本口恰计一次;通关局数按 summary.cleared 判定三关全通才计 1。终身计数写入
# 单口=本函数,禁第二写入点)。
func apply_adventure_summary(summary: Dictionary, boss_drops: Array = []) -> void: func apply_adventure_summary(summary: Dictionary, boss_drops: Array = []) -> void:
_data["meta"]["adventure_total"] = adventure_total() + 1
if bool(summary.get("cleared", false)):
_data["meta"]["adventure_cleared"] = adventure_cleared() + 1
add_keystone(int(summary.get("keystone_total", 0))) add_keystone(int(summary.get("keystone_total", 0)))
var cleared := int(summary.get("levels_cleared", 0)) var cleared := int(summary.get("levels_cleared", 0))
for li in range(1, cleared + 1): for li in range(1, cleared + 1):
+36 -6
View File
@@ -4655,7 +4655,7 @@ func _smoke_battle_log_view() -> void:
# ---- 存档序列化冒烟(SaveManager——读写往返/新档合法/余额拒花/锻造出域拒/高版本拒读禁写/ # ---- 存档序列化冒烟(SaveManager——读写往返/新档合法/余额拒花/锻造出域拒/高版本拒读禁写/
# 损坏兜底/迁移链/域界干净/摘要 cap/摘要集成)。测试文件统一 user://smoke_save_*.json 隔离, # 损坏兜底/迁移链/域界干净/摘要 cap/摘要集成/生涯终身计数域 v6)。测试文件统一 user://smoke_save_*.json 隔离,
# 段首清场+段尾清理——绝不碰 user://save.json(红线)。 # 段首清场+段尾清理——绝不碰 user://save.json(红线)。
func _smoke_save() -> void: func _smoke_save() -> void:
print("--- SaveManager 存档读写/迁移/兜底 ---") print("--- SaveManager 存档读写/迁移/兜底 ---")
@@ -4665,7 +4665,8 @@ func _smoke_save() -> void:
"user://smoke_save_corrupt.json", "user://smoke_save_migrate.json", "user://smoke_save_corrupt.json", "user://smoke_save_migrate.json",
"user://smoke_save_domain.json", "user://smoke_save_cap.json", "user://smoke_save_domain.json", "user://smoke_save_cap.json",
"user://smoke_save_integration.json", "user://smoke_save_breakpoint.json", "user://smoke_save_integration.json", "user://smoke_save_breakpoint.json",
"user://smoke_save_v2compat.json"] "user://smoke_save_v2compat.json", "user://smoke_save_v5upgrade.json",
"user://smoke_save_v5legacy.json"]
var stale: Array = [] var stale: Array = []
for p in paths: for p in paths:
stale.append(p) stale.append(p)
@@ -4687,6 +4688,8 @@ func _smoke_save() -> void:
"apply 原语入账(钥石 7+3、通关次数 1/1/0)") "apply 原语入账(钥石 7+3、通关次数 1/1/0)")
check(w.boss_first_kill_marked(1) and w.boss_first_kill_marked(2) and not w.boss_first_kill_marked(3), check(w.boss_first_kill_marked(1) and w.boss_first_kill_marked(2) and not w.boss_first_kill_marked(3),
"首杀标记随通关关卡置位(1/2 有、3 无)") "首杀标记随通关关卡置位(1/2 有、3 无)")
check(w.adventure_total() == 1 and w.adventure_cleared() == 0,
"生涯终身计数随 apply 自增恰一(中止局:总局数 1、通关局数 0——迭代 18 卡④)")
check(w.write_to_disk(), "写盘成功") check(w.write_to_disk(), "写盘成功")
var r = sm_script.new(paths[0]) var r = sm_script.new(paths[0])
check(r.load_from_disk(), "读回成功") check(r.load_from_disk(), "读回成功")
@@ -4697,6 +4700,7 @@ func _smoke_save() -> void:
check(r.clear_count(1) == 1 and r.clear_count(2) == 1 and r.clear_count(3) == 0, "通关次数表往返相等") check(r.clear_count(1) == 1 and r.clear_count(2) == 1 and r.clear_count(3) == 0, "通关次数表往返相等")
check(r.boss_first_kill_marked(1) and r.boss_first_kill_marked(2) and not r.boss_first_kill_marked(3), check(r.boss_first_kill_marked(1) and r.boss_first_kill_marked(2) and not r.boss_first_kill_marked(3),
"首杀标记往返相等") "首杀标记往返相等")
check(r.adventure_total() == 1 and r.adventure_cleared() == 0, "生涯终身计数往返相等(1/0)")
var s0: Dictionary = r.recent_summaries()[0] var s0: Dictionary = r.recent_summaries()[0]
check(r.recent_summaries().size() == 1 and int(s0.get("master_seed", -1)) == 99 and int(s0.get("keystone_total", -1)) == 3, check(r.recent_summaries().size() == 1 and int(s0.get("master_seed", -1)) == 99 and int(s0.get("keystone_total", -1)) == 3,
"冒险摘要往返相等(1 条,seed 99)") "冒险摘要往返相等(1 条,seed 99)")
@@ -4706,6 +4710,8 @@ func _smoke_save() -> void:
check(f2.load_from_disk() and f2.keystone_balance() == 0 and f2.forge_level("atk") == 0 check(f2.load_from_disk() and f2.keystone_balance() == 0 and f2.forge_level("atk") == 0
and not f2.item_unlocked("anything") and f2.clear_count(1) == 0 and f2.recent_summaries().is_empty(), and not f2.item_unlocked("anything") and f2.clear_count(1) == 0 and f2.recent_summaries().is_empty(),
"新档合法态(load true + 全零默认)") "新档合法态(load true + 全零默认)")
check(f2.adventure_total() == 0 and f2.adventure_cleared() == 0,
"新档生涯终身计数缺省全零(迭代 18 卡④两新域——白板存档完整可玩)")
# ③ spend 拒绝:余额不足 false 且零副作用(钥石只增不减——消费除外) # ③ spend 拒绝:余额不足 false 且零副作用(钥石只增不减——消费除外)
var f3 = sm_script.new(paths[2]) var f3 = sm_script.new(paths[2])
@@ -4748,8 +4754,8 @@ func _smoke_save() -> void:
fm.store_string(JSON.stringify({"version": 0, "keystone_old": 12})) fm.store_string(JSON.stringify({"version": 0, "keystone_old": 12}))
fm.close() fm.close()
check(f7.load_from_disk() and f7.keystone_balance() == 12, "v0 旧档经迁移链入账(keystone_old 12 → 余额 12)") check(f7.load_from_disk() and f7.keystone_balance() == 12, "v0 旧档经迁移链入账(keystone_old 12 → 余额 12)")
check(f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 5, check(f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 6,
"迁移链 0→1(测试钩)→5(生产) 写盘 version == 5(v5=天赋树域,迭代 12 T9——v4 被同调批占用顺延披露)") "迁移链 0→1(测试钩)→6(生产) 写盘 version == 6(v6=生涯终身计数域,迭代 18 卡④——v5=天赋树域迭代 12 T9)")
# ⑧ 域界干净:meta 永久进度域不含局内资源键(行动点/RAP/GP/道具库存/生命账本不入档——契约 2; # ⑧ 域界干净:meta 永久进度域不含局内资源键(行动点/RAP/GP/道具库存/生命账本不入档——契约 2;
# 迭代 8 卡①修订:扫描收窄到 meta 域——断点域=冒险进行中瞬态快照,生命/GP 按拍板面入域豁免) # 迭代 8 卡①修订:扫描收窄到 meta 域——断点域=冒险进行中瞬态快照,生命/GP 按拍板面入域豁免)
@@ -4846,8 +4852,8 @@ func _smoke_save() -> void:
fold.close() fold.close()
check(fo.load_from_disk() and fo.keystone_balance() == 4 and not fo.has_breakpoint(), check(fo.load_from_disk() and fo.keystone_balance() == 4 and not fo.has_breakpoint(),
"v1 旧档迁移 v4:断点域补空(合法空态,无断点域不炸)") "v1 旧档迁移 v4:断点域补空(合法空态,无断点域不炸)")
check(fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 5, check(fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 6,
"v1 旧档迁移后写盘 version == 5(v1→v5 链式——天赋树域 2026-10-04)") "v1 旧档迁移后写盘 version == 6(v1→v6 链式——生涯终身计数域 2026-10-07)")
# ⑫ v2 旧档→v3 兼容(迭代 10 g 裁——semver 向后兼容:旧断点零丢失、新代码可续玩) # ⑫ v2 旧档→v3 兼容(迭代 10 g 裁——semver 向后兼容:旧断点零丢失、新代码可续玩)
var fv2 = sm_script.new(paths[10]) var fv2 = sm_script.new(paths[10])
@@ -4866,6 +4872,30 @@ func _smoke_save() -> void:
check(adr_v2.resume_adventure(fv2.breakpoint_snapshot(), {"hero_override": {}}), check(adr_v2.resume_adventure(fv2.breakpoint_snapshot(), {"hero_override": {}}),
"v2 旧断点续玩受理(缺 bonus 附随件走常规重建路径——g 裁 semver 口径)") "v2 旧断点续玩受理(缺 bonus 附随件走常规重建路径——g 裁 semver 口径)")
# ⑬ 生涯终身计数域(迭代 18 卡④):v5 旧档升 v6 缺省补零 + 升档幂等 + 收束自增恰一
#(真实旧档形态=计数域两键不存在;升档只增不改——既有 meta 值零触碰,存档红线 S2 断言面)
var f13 = sm_script.new(paths[11])
var fv5u := FileAccess.open(paths[11], FileAccess.WRITE)
fv5u.store_string(JSON.stringify({"version": 5, "meta": {"keystone_balance": 9}}))
fv5u.close()
check(f13.load_from_disk() and f13.adventure_total() == 0 and f13.adventure_cleared() == 0
and f13.keystone_balance() == 9,
"v5 旧档升 v6:计数域缺省补零且既有余额原样(迁移只增字段不改值)")
check(f13.write_to_disk() and f13.load_from_disk()
and int(JSON.parse_string(FileAccess.open(paths[11], FileAccess.READ).get_as_text()).get("version", -1)) == 6
and f13.adventure_total() == 0 and f13.adventure_cleared() == 0,
"升档幂等:v6 档写盘再载零漂移(重复迁移零副作用)")
var f13b = sm_script.new(paths[12])
f13b.load_from_disk()
f13b.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 3,
"stopped_at": 0, "keystone_total": 0, "master_seed": 1})
check(f13b.adventure_total() == 1 and f13b.adventure_cleared() == 1,
"收束自增恰一·通关局(三关全通 cleared=true:总局数+1 且通关局数+1)")
f13b.apply_adventure_summary({"result": "abandoned", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": 0, "master_seed": 2})
check(f13b.adventure_total() == 2 and f13b.adventure_cleared() == 1,
"收束自增恰一·中止局(放弃局:仅总局数+1,通关局数不动)")
# 段尾清理(测试文件绝不残留) # 段尾清理(测试文件绝不残留)
for p in stale: for p in stale:
if FileAccess.file_exists(p): if FileAccess.file_exists(p):