Save Profile 运行时¶
GFSaveProfileUtility 用于协调一个项目存档中的多个独立模块。每个模块只实现
自己的 GFSaveSectionProvider,Profile 从 provider 清单派生当前文档 schema,
Storage 继续负责物理事务、编码和后台 IO。
适合使用 Save Profile 的场景包括自动保存、账号进度、背包、任务和设置等多个
长期维护边界需要共享一个版本化文档。只保存单个缓存字典时直接使用
GFStorageUtility;需要遍历场景节点时由 provider 把 GFSaveGraphUtility.gather_section()
接入一个 section,不要复制 Save Graph 格式。
定义 section provider¶
Provider 必须拥有稳定 section_id 和正数 schema_version。采集返回当前版本
GFSaveSection;应用失败时,Utility 会把失败 provider 和此前已尝试 provider
按逆序恢复到应用前快照。
class_name ProjectProgressSectionProvider
extends GFSaveSectionProvider
var progress: ProjectProgress
func _gather_section(_context: Dictionary = {}) -> GFSaveSection:
if progress == null:
return null
return make_section(progress.to_dict())
func _apply_section(section: GFSaveSection, _context: Dictionary = {}) -> Error:
if progress == null:
return ERR_UNCONFIGURED
var payload_value: Variant = section.get_payload()
if not payload_value is Dictionary:
return ERR_INVALID_DATA
progress.apply_dict(GFVariantData.as_dictionary(payload_value))
return OK
默认 _rollback_section() 会复用 _apply_section()。如果应用过程涉及额外缓存或
可撤销外部状态,应重写该钩子;不可回滚的网络请求、支付或平台副作用不能放进
普通 section 应用。终态中的每个 GFSaveRollbackFailure 都分别保存 section ID 和
Godot Error,调用方不应解析错误字符串来定位回滚失败。
注册 Profile¶
Save 扩展安装器会注册 GFSaveProfileUtility。项目在架构初始化完成后创建并注册
Profile;同一个 Utility 内的 profile ID 和文件名都必须唯一。
var profile := GFSaveProfile.new()
profile.profile_id = &"project.player"
profile.schema_id = &"project.player.document"
profile.file_name = "player/profile.sav"
profile.schema_version = 3
profile.providers = [progress_provider, inventory_provider, quest_provider]
var save_profiles := Gf.get_utility(GFSaveProfileUtility) as GFSaveProfileUtility
if save_profiles == null:
push_error("Save Profile Utility is unavailable.")
return
var registration: Dictionary = save_profiles.register_profile(profile, migrations)
if not GFVariantData.get_option_bool(registration, "registered"):
push_error("Player save profile registration failed.")
注册成功后,Utility 会编译并持有 profile ID、schema、规范存储路径、provider 清单和
恢复政策。之后修改 GFSaveProfile 不会改变运行时定义;provider 的身份和能力字段也
会被永久锁定。需要变更契约时,应等待 profile 空闲后注销,并使用新的
GFSaveProfile 与新的 Provider 实例重新注册,而不是复用或修改已锁定 Resource。
注册报告保留完整校验问题以及最终 canonical_file_name,可直接写入诊断。
GFSaveMigrationRegistry 仍是唯一迁移引擎。Profile 不提供字段级兼容回退;文档或
section 版本落后时必须存在完整迁移链,未来版本始终拒绝读取。
保存、flush 与读取屏障¶
每次保存请求获得递增 generation。同一 profile 只推进一个当前 IO;在途写入期间的 多个请求会合并为一次最新 generation 写入。较早句柄只有在覆盖其 generation 的写入 完成后才进入终态。物理写入超时后无法可靠取消时,原请求会 detached 并继续保有路径 所有权,因此它可能与后续重试短暂物理并存,但不再作为状态机的当前 IO。
var operation := save_profiles.save_profile(&"project.player", {
"slot_kind": "auto",
})
var result: GFSaveProfileResult
if operation.is_completed():
result = operation.get_result()
else:
result = await operation.completed
if not result.is_successful():
push_warning(result.get_error())
flush_profile() 捕获调用时可见的 generation,只在该 generation 或更新 generation
真实持久化后成功。load_profile() 同样捕获写入屏障;屏障写入失败时不会静默读取
旧文件。最老读取的屏障一旦满足,就会先获得一个调度轮次,避免持续自动保存让已就绪
读取长期饥饿;如果仍有保存等待,服务一个读取后会把下一轮交给保存,避免大量读取反向
饿死保存和 flush。尚未满足的读取仍等待能够覆盖其 generation 的保存。加载、迁移、应用
期间的保存请求会以 busy 明确拒绝,避免旧文件覆盖刚应用到内存的新状态。provider 和
状态变更回调中的重入请求会被拒绝;完成信号在状态稳定后发出,因此完成回调可以发起一个
新的非递归操作。
Profile 层使用 GFStorageAsyncOperation 的 request ID 观察自己的底层请求,不依赖
“文件名相同”的全局完成信号。因此其他调用方同时读写同一文件也不会完成错误句柄。
完成信号只在状态机恢复稳定状态后发出,回调可以安全查询快照或释放 Utility。
恢复政策¶
GFSaveRecoveryPolicy 默认对缺失和损坏文件返回失败。项目可以显式选择
ACTION_USE_CURRENT_STATE,此时读取成功终态会标记 recovered,但只保留当前内存
状态,不删除、替换或自动覆盖原文件。
var policy := GFSaveRecoveryPolicy.new()
policy.missing_file_action = GFSaveRecoveryPolicy.ACTION_USE_CURRENT_STATE
policy.retry_delays_msec = PackedInt32Array([50, 250, 1000])
policy.io_timeout_msec = 10_000
profile.recovery_policy = policy
重试只处理策略中列出的临时 Error,并使用有限延迟序列和单调时钟。未来 schema、
schema ID 不匹配、迁移失败和 provider 应用失败不进入损坏恢复。读取超时可以安全失败;
写入无法可靠取消,因此超时或写入期间释放会返回 outcome_unknown,调用方不得假定磁盘
未发生提交。未知证据按 generation 保存,结果中的 storage_request_ids 可用于对账;
超时请求迟到完成前,Profile 继续占有规范路径,注销和同路径重新注册都会被拒绝。
迟到的完成回调不会改变已经进入终态的操作句柄;如果同一逻辑保存仍在等待或执行重试,
任一覆盖其 generation 的物理写入成功会立即完成该句柄并取消尚未启动的重试。已经启动的
其他写请求继续由 Profile 保有路径直到物理终态,但其后续失败不会翻转已确认的保存成功。
未知 section 默认采用 GFSaveProfile.UNKNOWN_SECTION_REJECT,避免旧客户端读取后保存时
静默删除新客户端数据。只有明确拥有向前兼容要求时才选择 UNKNOWN_SECTION_PRESERVE;
选择 UNKNOWN_SECTION_DROP 等同于显式接受数据丢弃。
完整状态机、不变量和失败矩阵见 Save Profile 运行时编排决策。