- 将开发过程/归档文档迁至 docs_dev/(development_plan、certification_checklist、 已废弃的 Cocos 架构草案 archived_cocos_architecture_draft),并修正全部跨引用 - 新增根 README.md(项目介绍,暂定名 Spellforge)与 docs_dev/README.md 索引 - 新增 docs_dev/doc_code_audit_2026-07-20.md:文档 vs 代码交叉审计报告(经 6 路对抗性复核,零证伪),含「代码更优 / 文档更优 / 中性」判定汇总 - 在 docs/ 各设计·技术·机制文档就地加「实现现状 (2026-07-20)」callout: 追认代码更优实现(纯 JSON 数据驱动、SpatialGrid-only 碰撞、MultiMesh 单档、 存档选最新槽等),订正陈旧/矛盾内容(.tres→JSON、Boss HP/阈值/波次、EventID、 StatusManager.apply 签名等),标记未实现功能(C# 热路径、Mana、元进展、 Boss 阶段/抗性、Tutorial、轨迹/连锁/催化等)与 latent bug(CoreFeatureTag 位运算、 pierce 空操作、MAX_OPS 不读 cpu_limit) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
185 lines
9.4 KiB
Markdown
185 lines
9.4 KiB
Markdown
# 平台发行认证清单 (Platform Certification Checklist)
|
||
|
||
> **用途**:列出 Steam / Nintendo Switch 两个目标平台的强制通过项,并映射到对应开发切片。
|
||
> 每项完成后在 `[ ]` 处打勾,并记录验证者与日期。
|
||
> **规则**:所有 P0 项必须在 S6 发布前全部完成,P1 项视平台要求决定是否阻断发布。
|
||
|
||
---
|
||
|
||
## 一、Steam (PC) 必须项
|
||
|
||
### 1.1 隐私与数据合规(EU GDPR / Steam 要求)
|
||
|
||
| 编号 | 检查项 | 切片 | 状态 |
|
||
| :--- | :--- | :--- | :--- |
|
||
| ST-01 | 游戏不收集任何可识别玩家身份的数据(无账号系统、无上报遥测) | S2 | ⬜ |
|
||
| ST-02 | 崩溃日志(`user://crash_log.txt`)不包含 Steam ID / 机器码等 PII | S2 | ⬜ |
|
||
| ST-03 | 设置菜单提供「清除所有本地数据」选项(删除 `user://` 下所有文件) | S6 | ✅ 2026-06-05(`SettingsManager.clear_all_local_data`,二次确认;实测删除 save_data/endless_records/run_a·b/runs)|
|
||
| ST-04 | Steam 商店页包含隐私政策链接(即使只写"本游戏不收集任何数据") | S6 前置 | ⬜ |
|
||
| ST-05 | 如启用 opt-in 崩溃上报,玩家首次启动须明确同意 | S6(可选)| ⬜ |
|
||
|
||
> **ST-03 实现指引**:
|
||
> `SettingsManager.clear_all_local_data()` 遍历删除:
|
||
> `user://crash_log.txt`、`user://crash_log_prev.txt`、`user://run_a.json`、
|
||
> `user://run_b.json`、`user://endless_records.json`、`user://save_data.json`、
|
||
> `user://runs/`(截图目录)。调用后重置 `ProfileManager` 单例状态并返回主菜单。
|
||
|
||
### 1.2 Steam 成就 (Achievements)
|
||
|
||
| 编号 | 检查项 | 切片 | 状态 |
|
||
| :--- | :--- | :--- | :--- |
|
||
| ST-10 | Steam API 初始化成功(`Steam.init()` 返回 OK,或使用 GodotSteam 插件) | S6 | ⬜ |
|
||
| ST-11 | 所有成就在 Steamworks 后台已定义并与 EventBus 钩子对应 | S6 | ⬜ |
|
||
| ST-12 | 离线模式下成就解锁不崩溃(本地缓存后联网再同步) | S6 | ⬜ |
|
||
| ST-13 | 成就解锁调用幂等(重复触发不报错、不重复计数) | S6 | ⬜ |
|
||
|
||
> **EventBus 成就钩子**:`EventID.ACHIEVEMENT_UNLOCKED` **固定为 ID `18`**(负载:`{ "achievement_id": String }`),权威定义见 **`technical/implementation_plan.md` §2.1** 事件目录与 `event_ids.gd` 常量。
|
||
> `AchievementManager` 订阅此事件,调用 `Steam.set_achievement(achievement_id)` + `Steam.store_stats()`(须幂等,见 ST-13)。
|
||
|
||
### 1.3 Steam 输入与手柄
|
||
|
||
| 编号 | 检查项 | 切片 | 状态 |
|
||
| :--- | :--- | :--- | :--- |
|
||
| ST-20 | 手柄(Xbox / PS / Switch Pro)基本功能可用 | S1 | ⬜ |
|
||
| ST-21 | 手柄连接/断开时游戏不崩溃,自动切换输入方案 | S4 | ⬜ |
|
||
| ST-22 | 按键提示图标根据当前设备自动切换(Xbox A / PS × / 键盘 Space) | S6 | ⬜ |
|
||
|
||
### 1.4 Steam Deck 兼容性
|
||
|
||
| 编号 | 检查项 | 切片 | 状态 |
|
||
| :--- | :--- | :--- | :--- |
|
||
| ST-30 | 游戏在 Steam Deck 默认分辨率(1280×800)下可正常显示 | S6 | ⬜ |
|
||
| ST-31 | 所有 UI 文字在 Steam Deck 屏幕上可读(最小字号 ≥ 16px)| S6 | ⬜ |
|
||
| ST-32 | Steam Deck 触摸屏操作不引起误触崩溃 | S6 | ⬜ |
|
||
| ST-33 | Proton 兼容性验证(在 SteamOS 下通过 Proton 层运行测试) | S6 | ⬜ |
|
||
|
||
### 1.5 排行榜数据完整性
|
||
|
||
| 编号 | 检查项 | 切片 | 状态 |
|
||
| :--- | :--- | :--- | :--- |
|
||
| ST-40 | 本地 `endless_records.json` 写入时附加 HMAC-SHA256 签名 | S5 | ✅ 2026-06-05(`endless_records.gd`,对精确 JSON 字符串签名)|
|
||
| ST-41 | 读取时验证签名,签名失败则标记分数为「未验证」并拒绝提交 Steam 排行榜 | S5 | ✅ 2026-06-05(验签失败 `push_warning`+返回空,实测篡改拒绝)|
|
||
| ST-42 | Steam 排行榜提交时服务端异常(网络断开)不崩溃,本地缓存待下次提交 | S6 | ⬜ |
|
||
|
||
> **ST-40 HMAC 实现指引(C-4 修复)**:
|
||
> ```gdscript
|
||
> # endless_records_manager.gd
|
||
> # HMAC Key 存于 GDScript 编译常量(不含在存档内),对抗普通玩家手动编辑
|
||
> # 注意:本地 HMAC 无法对抗逆向工程,主要目的是防止随意手改
|
||
> const _HMAC_KEY: PackedByteArray = [0xA3, 0x7F, 0x2C, ...] # S5 前生成 32 字节随机密钥
|
||
>
|
||
> func _sign(data: String) -> String:
|
||
> var crypto := Crypto.new()
|
||
> var hmac := crypto.hmac_digest(HashingContext.HASH_SHA256, _HMAC_KEY,
|
||
> data.to_utf8_buffer())
|
||
> return hmac.hex_encode()
|
||
>
|
||
> func save_records(records: Array) -> void:
|
||
> var payload := JSON.stringify(records)
|
||
> var signed := { "data": records, "sig": _sign(payload) }
|
||
> FileAccess.open("user://endless_records.json", FileAccess.WRITE
|
||
> ).store_string(JSON.stringify(signed))
|
||
>
|
||
> func load_records() -> Array:
|
||
> if not FileAccess.file_exists("user://endless_records.json"): return []
|
||
> var raw := JSON.parse_string(
|
||
> FileAccess.open("user://endless_records.json",
|
||
> FileAccess.READ).get_as_text())
|
||
> if not raw is Dictionary: return []
|
||
> var expected := _sign(JSON.stringify(raw.get("data", [])))
|
||
> if raw.get("sig", "") != expected:
|
||
> push_warning("EndlessRecords: 签名验证失败,分数不可信")
|
||
> return [] # 拒绝载入被篡改的数据
|
||
> return raw.get("data", [])
|
||
> ```
|
||
|
||
---
|
||
|
||
### 1.6 Steam 云存档 (Steam Cloud Save)
|
||
|
||
| 编号 | 检查项 | 切片 | 状态 |
|
||
| :--- | :--- | :--- | :--- |
|
||
| ST-50 | `ProfileManager.save_run()` / `save_settings()` 通过 `Steam.beginFileWriteBatch()` / `Steam.endFileWriteBatch()` 写入,确保 Steam Cloud 同步 | S6 | ⬜ |
|
||
| ST-51 | `user://run_a.json`、`user://run_b.json`、`user://save_data.json` 均在 Steamworks 后台 Remote Storage 中启用 | S6 前置 | ⬜ |
|
||
| ST-52 | 跨设备(PC → Steam Deck)存档同步测试:PC 端存档可在 Steam Deck 读取并继续游戏 | S6 | ⬜ |
|
||
| ST-53 | Steam Cloud 配额不超标(Steam 默认 100MB/游戏;所有存档文件估算总量 < 1MB) | S6 | ⬜ |
|
||
|
||
> **ST-50 实现指引**:
|
||
> ```gdscript
|
||
> # profile_manager.gd
|
||
> func save_run(data: Dictionary) -> void:
|
||
> if Steam.is_steam_running():
|
||
> Steam.beginFileWriteBatch() # GodotSteam: 开始批量写入,触发 Steam Cloud 同步
|
||
> var which := get_int("run_write_slot", 0)
|
||
> var path := _SLOT_A if which == 0 else _SLOT_B
|
||
> var f := FileAccess.open(path, FileAccess.WRITE)
|
||
> if f:
|
||
> f.store_string(JSON.stringify(data))
|
||
> if Steam.is_steam_running():
|
||
> Steam.endFileWriteBatch() # 结束批量写入,触发 Steam Cloud 上传
|
||
> set_int("run_write_slot", 1 - which)
|
||
> ```
|
||
> **注意**:`beginFileWriteBatch` / `endFileWriteBatch` 仅在 `Steam.is_steam_running()` 时调用;离线模式下正常写本地文件,不崩溃。`Steam.is_steam_running()` 需 GodotSteam 插件支持。
|
||
|
||
---
|
||
|
||
## 二、Nintendo Switch(未来扩展)
|
||
|
||
> 以下为 Nintendo Lotcheck 最常见的阻断项,仅在立项 Switch 移植时启用。
|
||
|
||
### 2.1 系统级强制项
|
||
|
||
| 编号 | 检查项 | 优先度 |
|
||
| :--- | :--- | :--- |
|
||
| SW-01 | 游戏内按钮提示使用 Switch 图标(A/B/X/Y),不得显示 Xbox 图标 | P0 |
|
||
| SW-02 | Joy-Con 横持模式不崩溃(若不支持须在商品页说明) | P0 |
|
||
| SW-03 | 游戏可从睡眠状态恢复(`NOTIFICATION_APPLICATION_PAUSED` 处理) | P0 |
|
||
| SW-04 | 存档容量不超过 Switch 游戏卡限制(存档 < 32MB) | P0 |
|
||
| SW-05 | 不在联机功能中收集 Nintendo Account 信息 | P0 |
|
||
| SW-06 | Logo & Rating(CERO / PEGI / ESRB)在正确位置显示 | P0 |
|
||
| SW-07 | 游戏内截图功能不包含版权保护内容 | P1 |
|
||
|
||
### 2.2 性能要求
|
||
|
||
| 编号 | 检查项 | 优先度 |
|
||
| :--- | :--- | :--- |
|
||
| SW-10 | 掌机模式(720p)稳定 30fps(Switch 性能下限) | P0 |
|
||
| SW-11 | 内存使用峰值 < 2.5GB(Switch 总 RAM 4GB 共享系统/GPU) | P0 |
|
||
| SW-12 | 存档读写不阻塞主线程超过 100ms | P0 |
|
||
|
||
---
|
||
|
||
## 三、无障碍合规(Accessibility — 跨平台必须项)
|
||
|
||
> 参见 `architecture_design.md ADR-C1` 获取完整技术实现规范。
|
||
|
||
| 编号 | 检查项 | 切片 | 状态 |
|
||
| :--- | :--- | :--- | :--- |
|
||
| AC-01 | 元素标签同时用颜色 + 形状图标标识(色盲玩家可区分) | S3 | ⬜ |
|
||
| AC-02 | 所有 UI 字体支持缩放(50%~150%,设置项) | S4 | ⬜ |
|
||
| AC-03 | 高对比度模式(黑底白字选项)| S6 | ⬜ |
|
||
| AC-04 | 所有 VFX 提供「减少闪光」选项(防光敏性癫痫)| S6 | ⬜ |
|
||
| AC-05 | 游戏内所有音效有对应视觉提示(不依赖纯音频传达关键信息) | S6 | ⬜ |
|
||
|
||
---
|
||
|
||
## 四、评级申报
|
||
|
||
| 平台 | 机构 | 提交时机 | 状态 |
|
||
| :--- | :--- | :--- | :--- |
|
||
| Steam | IARC(自评系统,Steam 自动申报) | S6 发布前 | ⬜ |
|
||
| Steam(欧洲) | PEGI 自评或正式申请 | S6 发布前 | ⬜ |
|
||
| Switch | CERO(日本)/ PEGI / ESRB(需提前约 8 周提交) | 若立项 Switch | ⬜ |
|
||
|
||
---
|
||
|
||
## 五、发布前最终清单
|
||
|
||
| 编号 | 检查项 | 完成标志 |
|
||
| :--- | :--- | :--- |
|
||
| PRE-01 | Steam 商店页(截图 × 5、宣传片、描述文字)审核通过 | Steam 后台审核 OK |
|
||
| PRE-02 | 版本号 `1.0.0` 打 git tag,构建哈希记录在 `CHANGELOG.md` | git tag v1.0.0 |
|
||
| PRE-03 | 所有 P0 认证项状态为 ✅ | 本文档所有 P0 行 ✅ |
|
||
| PRE-04 | 发布分支通过完整 Wave 1~20 通关测试(无崩溃) | CI 通关测试日志 |
|
||
| PRE-05 | `crash_reporter.gd` 已注册为首位 Autoload,日志路径可写 | project.godot 验证 |
|