| 档案 | |
|---|---|
module |
games |
summary |
游戏平台:一个游戏 = 后端 games 表一行 + 一个文件夹的 H5;上线 = 改 live_version 一个整数,文件按 v{N} 不可变、只由后端发号;发布成功就有测试版(固定 /test/),按 Build 才有正式版;工人交活不拿钥匙,靠「在这间房」+ 三道机器闸 + 基线冲突 409,平台机器人代推 git 再直写托管存储;window.hoop 由 App 注入(不在任何仓里下载),hoop-kit 是手感库、烘焙进 AI 游戏;影子 id g_ = HMAC 按游戏隔离(昵称 / 头像另有规则,不是全都不可关联);榜四道闸;对战房状态只在 Redis,平台只在「回合归属」「最终胜负」两处权威,2 人房双确认 4 秒兜底,断线不立即判负(普通模式 30 秒宽限后仍可能判负,live 模式不因断线判负),Redis 结算与库里落 matches 不是一笔事务;约战卡 = game_invite 消息 + 60 秒呼叫窗;没做 verified 签分、观战、多游戏房;promote 本身没有回滚闸,只有探活那条有条件的尝试。 |
updated |
2026-09-08 |
verified_ref |
af743ff8bd9eee599a3168df5370c0284574f886 |
games_ref |
2f022816a9726f21e16c31e403b89a04adcfdb6f |
source_ref |
[backend/internal/domain/games, backend/internal/domain/room, backend/internal/domain/developer(发布 / 托管 / hoop-kit 那半), backend/internal/mcp/tools_game.go, backend/internal/mcp/tools_work.go(只引), backend/internal/domain/message/service.go(约战卡那段), app/lib/screens/game_webview_screen.dart, app/lib/screens/games_hub_screen.dart, app/lib/screens/game_.dart, app/lib/widgets/game_.dart, app/lib/widgets/active_match_bar.dart, hoop-games/lib/, hoop-games/deploy.sh] |
status |
current |
review_after |
2026-12-07 |
owner |
nova |
supersedes |
[GAME_PLATFORM_SPEC.md, GAME_KIT.md(§10 工坊除外 → rooms / studio), GAME_WORKERS.md, game_workers.html, PVP_GAME_INTEGRATION.md] |
partially_supersedes |
[GAME_STAGING.md(吸收 §五 as-built 与 §六;§一〜四 留作历史), PVP_ROOMS.md(吸收 §0b 一人一房与 §1 中继式权威;§3〜§7 规格作废、§5 那 9 个不存在的 SDK 名要删), PVP_ROOM_INTERNALS.md(只引;改房间代码仍看它), GAME_DEV_KIT.md(它是 SDK 面的唯一出处,本文只指路), GAME_INVITE_STANDARD.md(吸收 §9 as-built), game_workroom.html(吸收 ③ ⑦ ⑬② 三段判据;⑪ ⑫ 归 AI_BOTS), PVP_TEST_PLAN.md(只摘 5 条必过)] |
这份怎么读:每一节先讲「它解决什么、用户看到什么」,再讲「为什么这样做、数据在哪、哪一步会断、断了留下什么、有什么保障和缺口」;代码位置、参数、行号收在每段末尾的「出处」里。 出处标记:
[code@af743ff8 文件:行]= HOOP 仓那棵树上的实现;[games@2f022816 文件:行]= hoop-games 仓那棵树(本文的检查器不核这个仓);[prod@日期,谁核]= 生产机当天核过;[decision@日期,谁]= 拍板过的决定。 边界:游戏工作房的生命周期、工作台页签归 rooms;绿能量 / 内购 / 分成的规则归 wallet(本文只写游戏摸到钱的那几个口);素材管线、工坊(Skills / Assets / Services)、开发者身份归 studio;小程序是姐妹平台,归 studio / apps。「房」这个字在 HOOP 里有三个意思:对战房(本文 §3.6,Redis 里的)、游戏工作房(rooms)、四场景社群房(community);Jeff 08-12 说过「我们可不是为游戏而做的」,别把三个混成一个。
0. 给接手的人:三分钟读懂游戏平台
先认六个词:游戏(game) = 后端 games 表一行(id、entry_url、两个版本指针、挂哪间工作房)+ 一个文件夹的 H5(index.html 自包含);两个指针 = staging_version(测试版)和 live_version(正式版),都是整数,文件按 v{N} 目录不可变存放,上线 = 改一个整数,不搬文件;影子身份 = 游戏拿到的玩家 id g_…,是 HMAC 算出来的,按游戏隔离,游戏拿不到真实账号 id(昵称 / 头像按第 4 条另算);桥(window.hoop) = App 的 WebView 在页面一开始就注进去的对象,H5 只认它,任何仓里都下载不到;对战房(room) = Redis 里的一个房间状态,平台只裁「轮到谁」和「谁赢了」,不跑游戏规则;交活(submit) = 工人把整个文件夹交给平台,平台过三道闸后替它 push git、再直写托管存储。
它保证到什么程度:
1. 版本号只由后端发,发号先于写文件:脚本那条路先 POST …/deploys 拿号,交活直写那条路在 PublishFolder 里直接调 allocDeployVersion —— 两条路各自发号,但都进同一个 Go 函数、同一把每游戏顾问锁;脚本没令牌当场停、绝不回落到数本地目录;内容指纹相同(source_hash)不铸新号。
2. 「对象留存」「入口可访问」「响应缓存」是三件事:留存 —— 清理只留最近 5 版,live、staging、任何 Build 不删;可访问 —— 托管端点只放行 live 或 staging 命中的 v{N},别的 v{N} 就算对象还在也回 404;缓存 —— .html 一律 60 秒优先,命中的 v{N} 非 html 一年 immutable,其余(含整个 test/)60 秒。探活 —— 每 5 分钟探入口,连续两次探不到就尝试退到别的可达 Build(候选是「不等于当前」的 Build 倒序取 5,可能选到更高的一版);候选全探不通 = 尝试发房内提示「没有别的 build 可退」+ ops 红报;查候选 / 开事务 / 改指针 / 提交任一步报错 = 直接返回,不回退、也不保证发房通知(进入尝试前那一次 ops 红报已发过);只探入口,不证明玩法正常。
3. 工人零钥匙:交活的票就是「我在这间房」;三道闸(范围 → 账本 CHANGELOG 必须动过 → 基线没被别人推走否则 409)全在服务端;目标文件夹只信库里 pages_folder,永远不信 owner 能改的 entry_url;push 用 force:false 到 refs/heads/master,撞车 409 不硬推。git 推成、存储写坏 = 这版发布未完成、可能有残留对象(见 §1.1 断点),代码没有重试。
4. 影子 id 按游戏隔离,昵称和头像不是:g_ + HMAC(每游戏派生密钥, gameID|realUID)[:24],游戏 token 15 分钟。launch 正常就把真实昵称 name 和 avatar_url 一起给;只有外部游戏(isExternal)且未同意、或同意状态查询报错时把 name 置空,avatar_url 照给(指向公开头像端点,走 games 作用域的 persona 头像流,和真身头像是否同一张未核);自研 / 平台创作者的游戏不弹同意。所以能借昵称跨游戏对人的口不止对战房的 opponent.name(§4)。
5. 榜四道闸:metric 格式、分数 ≤ 2^53、必须真启动过这个游戏、30 次 / 分;live 对战另有每秒涨分上限,超了静默丢弃。没有 verified 签分(games.secret 列 Go 里零引用)。
6. 对战只在两处权威:回合归属(回合门)、最终胜负(2 人房双方各报一次,4 秒等不到另一方按单方兜底;不一致 = 散局无胜负;多人房房主一报即结,否则取唯一最高票 —— 不要求过半,并列 = 散局)。房间状态只在 Redis,库里只落最终 matches,但 Redis 结算和落库不是一笔:抢到结算标记 → Finish → SaveMatch 失败只记日志、不释放标记、照样发 room.ended 和改卡 —— 可能房已结束、卡已是记分板、库里没这局(补偿未核,§4)。断线不等于输:30 秒宽限且要「离线标记还在 + presence 没上线」两条都成立才判负;live 模式(g2048 / bubble / tetris / blockblast)永不因断线判负,3 分钟打完看分。
7. 一人一房(Jeff 07-08):进另一间房 = 旧房挂起,不是拒绝;leave 是挂起,forfeit 才是认输。
当前明确没做的(§4 全表,先说四条):verified 签分与 casual / verified 两档榜;观战(membership 是二元的,没有旁观者概念);多游戏协作房(一房一游戏唯一索引);批准 / 上线之后的「自动回滚」只有探活那一种,promote 本身没有回滚闸。
出了问题先查哪:§5 —— 「push 了手机上还是旧的」先分是 Pages 线还是托管线、再看是 test/ 60 秒缓存还是 v{N} 的一年缓存;「交活 422 / 409」按错误码对 §3.3 的表;「对战被判负 / 没结算」看 §3.6 的四个时钟。
1. 两段流程的一生
1.1 工人交一版 → 房里有测试版 → 按 Build 上线
你看到的:工作房里 AI 工人接卡、改游戏、交活;几秒后房里多一句「测试版已发 0.00N」,房里的人在工作台点「测试版」就能玩;满意了游戏主人(或房里的管理员 / 绑房的分身)按「Build」,正式版变成这一版,所有玩家重开游戏即新版。
背后发生的:
1. 取活:工人调 game_fetch → GET /v1/workrooms/{cid}/game,先过「是不是这间房的有效成员」;读文件走 game_file_read → …/game/raw/{path}(git master 那棵树的 blob 代理,≤ 8 MiB;没有 pages_folder 的游戏回 404 not_git_line)。工人没有 git 钥匙、没有部署令牌。
2. 交活 = 整个文件夹一次快照:game_submit → POST …/game/submit(30 MiB 上限)。三道闸:① 范围闸 —— 路径干净、扩展名白名单(.glb / .gltf 09-07 才加)、总量 20 MB、二进制按魔数验身(因为 Cloudflare Pages 对不存在的文件回「200 + 主页」,曾把三张「PNG」收成 index.html)、内容扫描(跨域 / 劫持)→ 各自 422;② 账本闸 —— CHANGELOG.md 必须在且 blob sha 必须和 HEAD 不同 → 422 no_changelog / stale_changelog;③ 基线闸 —— 第一次交随便,之后带的基线和 HEAD 不符 → 409 base_moved,提示里不许编造上一个提交人和时间。
3. 平台机器人代推:blobs → tree(仓里有、这次没交的文件会被删,因为交活是全量快照)→ commit 署「via HOOP platform submit, by 昵称」→ PATCH refs/heads/master 且 force:false,撞车 409 race;目标文件夹只从库里 pages_folder 取并再验一次 ^[a-z0-9][a-z0-9-]{0,63}$。
4. 直写托管存储(09-03 起):PublishFolder 把这个文件夹写进 hosted/{game}/v{N}/,写完读回 index.html 核一遍才动指针;内容指纹和当前测试版一样就不铸号;失败时 abort 尽力删已写对象(删错忽略),回 "cloud":"ci" + publish_error —— 这只是个标记,代码没有安排重试;CI 只在 hoop-games 仓 push master 时触发,触发凭据和是否真跑未核。「读回 index.html」只是 GetObject 成功即算,不比内容。第一次从 Pages 搬进托管线的游戏,先搬清单再切 entry_url,反了会 404。
5. 测试版就绪:staging_version = N,房里发系统消息;手机端「测试版」入口打 POST …/launch/staging,没权限的人后端静默回落正式版(不是 403)。
6. Build 上线:POST /v1/dev/games/{id}/promote → live_version = staging_version,写 game_promotions(kind 是算出来的:to ≥ from 是 promote,否则 rollback);谁能按 —— Jeff 09-08 定的规矩:游戏主人、这个工作室的管理员、房里的 AI,三种都能按测试版和正式版;代码现状(这棵树):owner / admin / 该游戏绑定工作房的现任成员分身(Vega 08-18 收紧,分身没绑房一律不能按)。Jeff 说「现在游戏里 admin 按不了」,李敏正让另一个窗口改;哪一层挡住了 admin 本文未核(§7)。
7. 玩家为什么重开即新版:正式版地址是 …/v{N}/,换版 = 换路径,老路径的一年缓存伤不到新版;换下来的 v{N} 要两个指针都不再指它托管源站才回 404(旧 live 仍是 staging 时照样能开),已缓存的副本客户端 / CDN 还可能回;Pages 线没有这道端点规则;App 的 WebView 又是 RELOAD_IGNORING_LOCAL_CACHE_DATA。
哪一步会断、留下什么:第 1 步不在房 = 403;第 2 步任一闸 = 422 / 409,git 一个字节没动;第 3 步 GitHub 撞车 = 409 race,重取基线再交;第 4 步写存储失败 = git 已推、这版发布未完成、可能有残留对象(首发时 importPagesBuild 已搬进来的旧 live 对象也在),回 cloud: ci 但没人保证 CI 会补跑;第 5 步之前 test/ 都是上一版;第 6 步 admin 现在按不了(Jeff 09-08 说的,待另一个窗口修);promote 之后没有回滚闸,只有探活那条有条件的尝试(§3.2);Pages 线的游戏(按 CHANGELOG 09-03 的记录还剩 ghostsurvivors / hexgl / texas,此刻分布未核)走的是 deploy.sh 整仓快照,不走第 4 步。
出处:取活 / 读文件
[code@af743ff8 backend/internal/mcp/tools_game.go:14-35]、房成员门[code@af743ff8 backend/internal/domain/games/service.go:212-219],raw 代理[code@af743ff8 backend/internal/domain/games/gitfiles.go:208];三道闸[code@af743ff8 backend/internal/domain/games/submit.go:42-43,84,100,109-154,179-193,206-209],基线判决:305-316、提示不编造:337-359、全量快照删文件:466-471、force:false:491-496、发布失败降级:522-529;直写托管[code@af743ff8 backend/internal/domain/developer/publish.go:30-66,73,80-139](abort 删残留:88-90,读回只GetObject:105-110);发号先于写[code@af743ff8 backend/internal/domain/developer/developer.go:1574];staging 静默回落[code@af743ff8 backend/internal/domain/games/service.go:337-339,382-386];promote / rollbackdeveloper.go:1841-1882,kind派生[code@af743ff8 backend/internal/domain/games/repository.go:619];发布权[code@af743ff8 backend/internal/domain/developer/release_verdict_test.go:1];手机 staging 分流[code@af743ff8 app/lib/api/api_client.dart:3790-3800],忽略本地缓存[code@af743ff8 app/lib/screens/game_webview_screen.dart:1136-1145];守卫baseline_test.go、submit_folder_test.go、submit_hint_test.go、submit_glb_test.go、deploy_race_test.go[decision@2026-08-11,Jeff:「满意了按一个按钮就变成 production,修改的时候不影响线上版本」][decision@2026-08-15,Jeff:工人零钥匙、三道机器闸、平台代推;「每个游戏这样加我会死!」→ 主令牌][decision@2026-09-02,Lisa:有代码就有测试版 0.001,按 Build 才有正式版,live 0 = 不上平台][decision@2026-09-03,记录署名不一;2026-09-08 Jeff 语音重申:AI 自己交、必走托管线]。
1.2 约一局对战:从聊天里的卡到结算
你看到的:聊天「+」选一个 PVP 游戏 → 群里出现一张约战卡(268×150),对方手机响 60 秒邀请铃 → 对方点「接受」直接进游戏 → 两人各自出招,回合制有 30 秒射钟,live 模式 3 分钟看分 → 打完卡变成记分板;中途谁断线,卡不会立刻判他输。
背后发生的:
1. 建房:room.create {game, max:2, conv_id} 走 WS。壳无条件把 game 覆盖成当前页面的 gameId(身份归壳不归页面);游戏名过 gamegate(60 秒缓存的可玩名单,库挂了用旧名单,冷启动 + 库挂拒绝);live 还是回合制由游戏 id 推断,不是客户端参数。房状态写 Redis room:{rid},TTL 2 小时。
2. 卡:开放房(群里)= SendGameInviteGroup,定向(1 对 1)= SendGameInvite;都是 messages.type='game_invite',body 带 game / room_id / mode / max / joined / status / expires_at(2h,= 房 TTL) / ring_until(60s,= 占座窗,同一个常数引用不抄)。拉黑查两次(room 域发横幅前一次、message 域发卡前一次),因为「卡失败不阻止邀请」这条契约让单一处检查漏一条路。对方 WS 不在线才发 APNs,铃声 game_invite。
3. 进房:room.join 门禁在 Lua joinScript 里原子判(私房要在 rinv 名单里,09-02 评审后收紧);2 人房加入即自动开局(waiting → ready 只做一次,没有「点准备」)。手机端接受 = push 游戏页带 joinRoomId,壳调 __hoopAutoJoin(rid)。所有卡在房状态变化时整批重写(不是只改最新一张),广播 message.edited;有人进了就删 ring_until,卡不能再说「没人接」。
4. 对局:room.action(回合门,不是你的回合 → not_turn)/ room.endturn(snapshot) / room.say(不过回合门,10 次 / 秒);快照 shadow-keyed,只收自己那段 ≤ 2 KB;射钟 30 秒既挂进程内 AfterFunc 又写 durable ZSET,room.ping 每 5 秒带权威回合信息,H5 侧对不上就本地合成一条 room.turn{reconciled:true} 自愈。
5. 断线:房级心跳 12 秒没到 → markPeerGone → 30 秒宽限 → judgeGrace;判负前两条都要成立:离线标记还在 且 presence:online:{uid} 不存在。live 模式跳过这一整段,3 分钟到点看分,平局 = 散局。
6. 结算:2 人房各报 room.result,Lua 返回 pending / agree / dispute / already;4 秒等不到对方按单方;dispute = abandoned 无胜负;多人房房主一报即结、否则取唯一最高票(不要求过半)。收口 finalizeResult:FinalizeOnce 抢标记 → 取房失败 / Finish 失败会释放标记 → SaveMatch 落 matches 失败只记日志、不释放,接着照样 pushEnded + finishCard。
哪一步会断、留下什么:第 1 步游戏名不在名单 = room.err;第 2 步被拉黑 = 横幅和卡都没有;第 3 步 not_invited / room_full / room_closed / not_found = 手机 toast 一次并把这个 rid 记进 rejected 不再重发;第 4 步超射钟 = 自动过回合;第 5 步宽限内回来 = 继续,leave 只是挂起;第 6 步双方都不报 = 房 2 小时 TTL 到期,卡跟着房走;第 6 步落库失败 = 房已结束、卡已是记分板、matches 没这局,补偿未核;手机端「回到对战」悬浮条数据全来自服务器 room.resume,没有本地猜。
出处:身份归壳
[code@af743ff8 app/lib/screens/game_webview_screen.dart:930-943];gamegate[code@af743ff8 backend/internal/domain/room/gamegate.go:28,51-70];live 由 id 推断[code@af743ff8 backend/internal/domain/room/manager.go:78-89],状态机:32-45,TTL:48,占座窗:459;卡[code@af743ff8 backend/internal/domain/message/service.go:1807,1811-1836,1876,1925-1998],拉黑两处[code@af743ff8 backend/internal/domain/room/ws.go:76-82,209-212];WS 全表ws.go:137-715,join 门禁 Lua[code@af743ff8 backend/internal/domain/room/manager.go:27,174-181];时钟ws.go:13-33,688-697,1076-1167;断线两条判据ws.go:1268-1284;结算ws.go:414-509,910-925,结算 LuaresultScriptmanager.go:232-241,finalizeResult与落库失败不释放ws.go:856-877,FinalizeOncemanager.go:561-570;手机卡[code@af743ff8 app/lib/widgets/game_invite_card.dart:109-158,188-189,302-376],全局弹窗[code@af743ff8 app/lib/widgets/game_invite_listener.dart:73-144,180-240],悬浮条只信服务器[code@af743ff8 app/lib/widgets/active_match_bar_host.dart:4-8,59-98],拒绝码不重发game_webview_screen.dart:830-862;守卫room/invite_gate_test.go、block_gate_test.go、live_test.go、ws_flow_test.go、message/gameinvite_db_test.go、app/test/room_game_identity_test.dart、game_bridge_join_reject_0903_test.dart[decision@2026-07-08,Jeff:一人一房][decision@2026-07-14,Jeff:邀请卡六条][decision@2026-08-21,Jeff:「我也不可能邀请你两个小时」→ 60 秒呼叫窗][decision@2026-09-02,评审 #6:私房要在 rinv 里才能进]。
2. 用户能看到的能力(怎么用 / 限制 / 入口)
| 能力 | 怎么用 / 限制 | 后端入口 | App 落点 |
|---|---|---|---|
| 游戏货架 | 只列 status='active' AND live_version>0;收藏架 + 最近在玩;快照先画、空列表不存 |
GET /v1/games、/v1/games/mine |
games_hub_screen |
| 玩一局 | 正式版永远 live;测试版入口没权限静默回落;外部游戏首次先问「分享资料给这个游戏?」,拒绝 = 昵称置空、头像照给;自研游戏不问 | POST …/launch、…/launch/staging |
game_webview_screen、game_profile_consent_sheet |
| 榜 | 好友榜 / 全球榜(top 默认 50 上限 200);四道闸;日榜 48h 周榜 9d | POST …/scores、GET …/leaderboard/* |
底栏 🏆、game_stats_screen |
| 我的战绩 | 单游戏三 tab(好友 / 世界 / 挑战);跨游戏形态在固定源码里没有入口接到(线上未核) | GET …/records/mine、/v1/games/stats/mine |
game_stats_screen |
| 约战 | 聊天「+」只列 pvp=true 的游戏(不写死);定向 / 开放两种卡;群里开放卡故意没有「拒绝」 |
WS room.create / invite / join / decline / close |
game_invite_picker、game_invite_card、game_invite_listener |
| 回到对战 | 全局悬浮条,数据只来自 room.resume |
WS room.resume |
active_match_bar_host |
| 云存档 | 每人每游戏一份 JSON ≤ 16 KB;data:null 不是错;读库失败 503 不给假空档 |
GET|PUT …/save |
桥 hoop.save.* |
| 商城 / 广告 / 库存 | H5 只拿 {ok},买没买要再问 inventory;余额不够 → 充值页(wallet) |
…/shop*、…/inventory、…/ad |
game_shop_sheet |
| 画作库(Pictionary) | 投稿 / 看 / 猜 / 同桌 | …/drawings* |
桥 hoop.gallery.* |
| 社交(赞 / 收藏 / 分享 / 评价) | 原生底栏;H5 里的社交条只是本地计数(桥没有 social API) | …/social、…/like|favorite|share|review |
game_bottom_bar |
| 出品方 | 看 / 关注 / 进主页 | creator.* 桥 → 已有接口 |
game_webview_screen |
| 工人取活交活 | 房成员才行;交活整包 ≤ 20 MB / 300 文件;三道闸 + 基线 | GET …/game、…/game/raw/*、POST …/game/submit |
MCP game_fetch / game_file_read / game_submit |
| 测试版 / Build / 退回 | 房里人玩测试版;Build 只 owner / admin / 绑房分身;退回只能到 is_build 版且先探路径 |
…/promote、…/rollback、…/deploys |
工作台第 5 页(rooms)、hosting_workspace_screen(studio) |
| 直发(CI / 脚本) | 令牌口:发号 → 就绪 → 回执;409 = 别人抢先,不是失败 | POST /v1/games/{id}/deploys、…/{ver}/ready、…/{ver}/ci、…/publish-from-git |
hoop-games/deploy.sh |
| 手感库 | AI 游戏烘焙进去、手写游戏自己 <script src>;清单从源码解析,不手维护 |
GET /v1/games/kit |
hoop-games/lib/hoop-kit.js |
| H5 报错遥测 | 免鉴权,10 / 分 / IP,1 KB | POST /v1/errlog |
hoop-games/lib/pvp-live.js |
出处:路由
[code@af743ff8 backend/internal/domain/games/handler.go:48-76]、[code@af743ff8 backend/internal/domain/games/save.go:46-47,70-75]、[code@af743ff8 backend/internal/domain/games/social.go:278-283]、[code@af743ff8 backend/internal/domain/games/gallery.go:384-388]、[code@af743ff8 backend/internal/domain/games/stats.go:11-13,135];developer 路由[code@af743ff8 backend/internal/domain/developer/developer.go:113-178];货架[code@af743ff8 app/lib/screens/games_hub_screen.dart:154-158,383,557-561],选人面板不写死[code@af743ff8 app/lib/widgets/game_invite_picker.dart:53-80],战绩跨游戏形态不可达[code@af743ff8 app/lib/screens/game_stats_screen.dart:33-36]。
3. 内部怎么运作
3.1 注册表、两条托管线、一个文件夹
用户看到的:货架上一个游戏 = 一张封面 + 一个名字(中英分列,不混);点进去是一个网页。
怎么做的:games 表一行(000055 起):id(短码,和文件夹名不一定相同:2048 → g2048、block-blast → blockblast 是 5 个特例)、entry_url、status(active / draft / review / disabled)、live_version / staging_version、workroom_id(一房一游戏唯一索引)、pages_folder(只有管理员能写,交活和发号都只信它)、deploy_token、cover_key、pvp(000274 一次性回填 19 个,Go 里没有写路径,新 PVP 游戏不改 SQL 就不会出现在约战面板)、name_zh / name_en。文件两条线:托管线 = R2 hosted/{game}/v{N}/{path},域名 games.hoopcomm.com(CHANGELOG 09-03 记录当天 49 个 active 搬到这条线);Pages 线 = hoop-games.pages.dev/{folder}/v{N}/ 与 /test/,由 deploy.sh 整仓快照上传(同一记录说剩 3 个)。这两个数是那天的盘点,此刻线上分布未核。H5 仓一个游戏 = 一个顶层文件夹 + index.html(部署脚本只认 depth-2 的 index.html,08-29 tools/ 折成幽灵游戏红了 13 次之后定的);仓里没有清单文件,注册是后端的事,deploy 时后端回 404 = 「这个文件夹不是游戏」。
出处:表
[code@af743ff8 backend/migrations/000055_games.up.sql:5-26],指针 000140 / 000205,pages_folder000260 与 admin 写[code@af743ff8 backend/internal/domain/developer/developer.go:2907-2932],pvp[code@af743ff8 backend/migrations/000274_games_pvp.up.sql:6-18],名字 000287 / 000338;两条线developer.go:228-232、[code@af743ff8 backend/internal/domain/developer/publish.go:30];H5 仓布局[games@2f022816 deploy.sh:76-93],folder→id 只留一行[games@2f022816 deploy.sh:147-155],404 = 不是游戏[games@2f022816 deploy.sh:364-366];列表门[code@af743ff8 backend/internal/domain/games/repository.go:234,273][decision@2026-09-04,Jeff:「不可以有中英文混在一起」→ 000338]。
3.2 发布与版本:两个整数、四种缓存、一条探活
用户看到的:发布成功后可以打开测试版(0.00N);按 Build 可把测试版设为正式版。正式版入口连续两次探测失败后,系统尝试切到其他可达 Build;没有可达候选时会尝试发一句房内提示。查询或事务失败可能直接返回,不能依赖一定收到房内通知;实际通知还取决于通知回调和工作房配置(say 在回调为空或房 / 开发者 id 为空时直接返回)。
怎么做的:版本号只由后端发(§0 第 1 条);test/ 是固定别名不是 /v{N}/(08-30 定的,链接本身不变;能不能打开仍取决于 staging 指针和对象在不在);端点先判可访问:test/ 映射到 staging(staging 为 0 回 404),v{N} 只放行等于 live 或 staging 的那两版,其余 404;再定缓存:.html 一律 60 秒优先、命中的 v{N} 非 html 一年 immutable、其余(含整个 test/)60 秒、封面 3600;test/ 曾被打成 immutable 在边缘 HIT 了 8 小时,所以显式排除。Pages 线 _headers 是 no-cache,靠边缘 304;但 09-07 发现 Cloudflare 边缘对 lib/*.js 存了一年 immutable 副本,几天不更新,当时用 ?v=8 绕过 —— 「推完重开即新版」在托管线的旧对象上不成立。清理:留最近 5 版,永不删 live / staging / is_build;deploy.sh 那边同样三道守卫(KEEP_LIVE / KEEP_STAGING / ALL 哨兵)且上传前先把云上的 live / staging / 每个 Build 按文件逐个核齐,缺就从 CDN 拉回来(「有 index.html ≠ 这一版完整」,sheep v26 少一张 PNG 差点半版上线)。探活:reconcile.go 每 5 分钟探入口,连续两次探不到 → 查「is_build 且版本不等于当前」倒序取 5 个候选、逐个探,退到第一个探得通的(可能比当前更高);候选全探不通 = 尝试发房内提示「连续 10 分钟探不到且没有别的 build 可退」+ ops 红报;查候选 / Begin / Exec / Commit 报错 = 直接返回,不回退、不保证房通知;staging 只喊不回滚。日出口流量 10 GB / 游戏 → 429。
出处:可访问与缓存
[code@af743ff8 backend/internal/domain/developer/developer.go:961-1009],egress:209,944-951,清理:1019-1053;test/别名[code@af743ff8 backend/internal/domain/games/service.go:263-316];探活与回退[code@af743ff8 backend/internal/domain/developer/reconcile.go:9-14,114,178-221];H5 仓_headers[games@2f022816 _headers:2-5],边缘死缓存[games@2f022816 CHANGELOG.md:542-569],三道清理守卫[games@2f022816 deploy.sh:376-436],逐文件核齐[games@2f022816 deploy.sh:232-289],发号不回落[games@2f022816 deploy.sh:142-143,352-372],守卫[games@2f022816 guard_keep_builds.sh:21-37](没接进任何流水线);守卫staging_url_test.go、version_label_test.go、desk_versions_test.go[decision@2026-08-30,Jeff:Build 永不清理;「小改动只是 changelog 里的一条记录,而 build 是真正发出去的那一次」]。
3.3 工人取活交活
见 §1.1。要点补三条:任务卡的取 / 报 / 收(work_task_*,queued → working → done|failed|cancelled,租约 5 分钟、3 分钟没心跳算陈)归 rooms,本文只引;两条流水线(交活直写 vs deploy.sh 整仓)HTTP 入口不同(直写在 PublishFolder 内调函数,脚本打 POST …/deploys),但都进同一个 allocDeployVersion 和同一把每游戏顾问锁;托管线以外的「hosted submit」是 501 not_yet 的桩,注释承诺的下一刀没来。
出处:
[code@af743ff8 backend/internal/mcp/tools_work.go:69-195]、[code@af743ff8 backend/internal/domain/workroom/workroom.go:60,68,833-841,878-883];501 桩[code@af743ff8 backend/internal/domain/games/submit.go:18,206-209]。
3.4 桥、手感库、模拟器:三样东西别混
用户看到的:H5 游戏里能看到自己的头像和名字、能交分、能开商城、能约战;开发者在浏览器里也能跑起来。
怎么做的:(a) window.hoop 是桥,由 App 的 WebView 在 AT_DOCUMENT_START 注入(_hoopJs()),H5 调 callHandler('hoopCall', method, args),Dart 侧一个 switch 接住;room.* 一律直发 WS,_ws.reconnect / _conn.down 两条只在壳内;注入完成发 hoopready;分组新名(hoop.user / score / friends / share / ui / shop / ads / save / creator / room)和旧名完全等价,守卫扫源码比对 JS 名与 Dart switch,另一守卫比对 GAME_DEV_KIT.md。(b) hoop-kit.js 是手感库(v1.0.1,九个模块),零网络零导航,因为它是在安检之后烘焙进 AI 生成的 HTML 的,而安检是纯文本匹配(连注释都扫);源头在 hoop-games 仓 lib/,后端那份由 tools/sync_hoopkit.sh 生成,/v1/games/kit 的清单是从源码解析的;未知属性返回可链式的 no-op 并只警告一次(AI 会编 API)。(c) hoop-sim.js 是模拟器,window.hoop 已在就自动退位;hoop-compat.js 是名字垫片,只补缺、不覆盖、搬同一个函数对象(桥跟 TestFlight 走、游戏跟 Cloudflare 走,节奏对不上);pvp-live.js 是共用对战流程库(接新 live 游戏还要在后端 liveGames 加一行,漏了就按回合制跑永不结算);social-bar.js 是本地计数的社交条,App 画了原生的它就自隐。模拟器不是 1:1:room.decline、creator.*、gallery.*、shop.buy 都没有,注释说的「1:1 对齐」不成立。
出处:注入与
switch[code@af743ff8 app/lib/screens/game_webview_screen.dart:720-909,913-1108,1146-1167],room.*直发:930-943;守卫app/test/game_bridge_method_names_test.dart、sdk_doc_matches_bridge_test.dart;hoop-kit[code@af743ff8 backend/internal/domain/developer/hoop-kit.js:9-27,533-568],清单解析[code@af743ff8 backend/internal/domain/developer/kit_manifest.go:11-31];H5 仓[games@2f022816 lib/hoop-sim.js:23-47,132-136]、[games@2f022816 lib/hoop-compat.js:8-33,52-78]、[games@2f022816 lib/pvp-live.js:8-63]、[games@2f022816 lib/social-bar.js:24-32,373-387][decision@2026-08-05,Jeff:kit A 案烘焙,「可以,建最牛的」][decision@2026-08-21,Jeff:「游戏那边为什么还用旧名字 赶紧做」→ hoop-compat]。
3.5 身份、榜、资料同意
怎么做的:影子 id 见 §0 第 4 条;游戏 token HS256 15 分钟(parseGameToken 没有调用方,留给未来网关);LaunchResult 正常带真实昵称 + avatar_url;ProfileSharingGate 回 (isExternal, consented):外部且未同意 → name="" 且 NeedsProfileConsent=true,查询报错 → name="" 但不问(保守侧);同意记 consent_records 键 game:<id> 后重新 launch;拒绝 = 昵称置空、头像照给、下次再问;自研游戏不弹。榜在 Redis ZSET(lb:{game}:{metric}:{period}),好友榜读时现算 O(好友数)不预生成;四道闸见 §0 第 5 条;/v1/games/{id}/avatars/{guid} 公开,guid 本身就是能力令牌;/v1/games/stats/mine 故意不收 user_id 参数,否则成了去匿名化的神谕。头像流走 AvatarStreamer 接口注入。
出处:
[code@af743ff8 backend/internal/domain/games/identity.go:16-18,39-42,66-67];昵称 / 头像与同意[code@af743ff8 backend/internal/domain/games/service.go:408-425]、[code@af743ff8 backend/internal/domain/games/model.go:69]、[code@af743ff8 backend/internal/domain/games/repository.go:568]、手机[code@af743ff8 app/lib/screens/game_webview_screen.dart:544-572]、[code@af743ff8 app/lib/widgets/game_profile_consent_sheet.dart:12-17];榜与闸[code@af743ff8 backend/internal/domain/games/service.go:134-138,447-496,569-571];公开头像[code@af743ff8 backend/internal/domain/games/handler.go:5-6,415-433];守卫leaderboard_guard_test.go、stats_test.go。
3.6 对战房:状态在 Redis,权威只两处
见 §1.2。补四条:跨节点广播复用单一 hoop:events 频道全量扇出(按房分频道是次级欠账);控制流有进程内存(AfterFunc)+ durable ZSET 兜底,每 2 秒扫 500 条 / 类;room.dbg 是自称临时的调试通道,还挂着;ErrNotTurn 声明了没人返回,WS 层直接发字符串 not_turn。限流:say 10 / 秒、action 20 / 秒、create 15 / 分、invite 10 / 分。改房间代码看 PVP_ROOM_INTERNALS.md,本文不吞它。
出处:
[code@af743ff8 backend/internal/domain/room/manager.go:1-8,25,719-755]、[code@af743ff8 backend/internal/domain/room/ws.go:13-33,710,1076-1127]。
3.7 云存档与足球阵容
怎么做的:game_saves(user_id, game_id, data JSONB),16 KB 上限,只校验大小,不校验形状;save.go 的注释写 flicksoccer 是唯一在用的游戏(17 人)—— 那是写注释那天的盘点,不是本文核的生产事实。足球阵容「PvP 总点恒定 25 / 人」「成长只进 PvE」「钱只买个性」这些规则只在图纸里,后端没有任何校验(SOCCER_SQUAD.md 标 P3 待做);阵容同步靠 room.say({t:'sq'}),隐含依赖 say 10 / 秒且不过回合门。
出处:
[code@af743ff8 backend/internal/domain/games/save.go:22,46-75],表 000139;[code@af743ff8 backend/migrations/000274_games_pvp.up.sql:16][decision@2026-07-11,Jeff:「成长=个性,不是数值碾压」]。
3.8 手机端壳:围栏、退避、缓存
怎么做的:单例壳(上一个实例的路由被 removeRoute);导航围栏纯函数 gameNavVerdict:同 host 且同第一段路径才放行,主框架的 data: / blob: 拒,外域主框架开系统浏览器;加载失败四条捕获 → 退避 1 / 3 / 6 秒后亮失败页,启动失败用 friendlyError 不给原始异常;返回键先判「页内能不能后退」(去 hash 后不同且同 host 同第一段)再退出;退出前 hoop.room.leave()(挂起,不是认输)。缓存:last_game_id、首帧底色、自绘返回键名单、货架快照 gameshub:all|mine(空不存)、战绩 / 分析按 key 分快照、game_names_v2(键带版本);兜底货架三款;没有指数退避,只有 WebView 那一处。心跳 5 秒 / 看门狗 12 秒 / 强制重连节流 8 秒全在注入的 JS 里。
出处:
[code@af743ff8 app/lib/screens/game_webview_screen.dart:142-166,226,403-418,518-542,894-905,1171-1197,1447-1483,1819-1862];[code@af743ff8 app/lib/widgets/game_back_button.dart:34-66];[code@af743ff8 app/lib/widgets/game_load_error_view.dart:11-15];快照[code@af743ff8 app/lib/screens/games_hub_screen.dart:154-158,383,417-437],[code@af743ff8 app/lib/util/game_names.dart:42-44];守卫game_nav_fence_test.dart、game_load_error_golden_test.dart、game_back_in_page_0908_test.dart[decision@2026-09-04,Jeff:「我不要有原生的那个 back button」→ chrome({back:'own'})][decision@2026-09-07,賈斯特:返回应回到原本的游戏页面]。
4. 还没做的(不是现状)
| 项 | 状态 | 出处 |
|---|---|---|
| verified 签分 / casual-verified 两档榜 | 未做;games.secret 列 Go 零引用 |
000055:10;GAME_PLATFORM_SPEC §3 |
| 观战 | 未做,membership 二元 | room/ 全包无 spectator |
| 多游戏协作房 | 一房一游戏唯一索引 | 000206;GAME_STAGING §六⑦ |
| promote 之后的回滚闸 | 只有探活自动回滚;人工 rollback 只能到 Build | reconcile.go、developer.go:1841-1882 |
| 交活 git 已推、发布未完成 | abort 尽力删残留(删错忽略),回 cloud:ci 标记,代码没有重试;CI 是否补跑未核 |
submit.go:522-529、publish.go:80-139 |
| 结算落库失败 | Redis 已结束、卡已改、matches 没这局;finalizeResult 这条分支不释放标记、没有补偿,别处有没有补偿待核 |
room/ws.go:856-877 |
| 探活回退 | 只在有可达候选时尝试,候选不限于更低版本;候选全探不通只尝试发房内提示;查询 / 事务报错直接返回 | reconcile.go:178-221 |
| 新 PVP 游戏进约战面板 | games.pvp 没有 Go 写路径,要手写 SQL |
000274:6-8 |
| hosted 线 submit | 501 not_yet 桩 |
submit.go:206-209 |
| 影子身份被昵称削弱(M3)、token 走 URL(M2) | 待做 | PVP_ROOM_INTERNALS §8 |
房间状态改动非全原子(坎 2)、单一 hoop:events 全量扇出、sweeper 全实例重复扫 |
待做 | PVP_ROOM_INTERNALS §5 / §7 / §9 |
| 云存档形状 / 足球点数校验 | 未做,只校 16 KB | save.go:22;SOCCER_SQUAD P3 |
invite 桥接自家选人界面 |
仍回落系统分享,GameInvitePicker 存在但没接到桥上 |
game_webview_screen.dart:1020-1029 |
nav 只有 home / records;nextGame = 退出 |
其余 not_built |
game_webview_screen.dart:1030-1045 |
跨游戏战绩页、/v1/games/stats/mine |
固定源码里唯一构造点永远传 gameId,入口没接到;线上未核 | game_stats_screen.dart:33-36 |
guard_keep_builds.sh 接进流水线 |
没接,没人跑 | hoop-games 根 |
45 个托管游戏的 live_version 指针陈旧 |
hoop-games CHANGELOG 09-07 记录:只切了 2 个,其余「知道那批怎么传的人来收尾」;此刻状态未核 | hoop-games CHANGELOG:591-592 |
托管线 CSP img-src 挡出品方头像 |
两个选项写了没拍 | hoop-games CHANGELOG:564-569 |
| 真机双端 PVP 验收(PVP_TEST_PLAN) | 跑没跑没人回填 | PVP_TEST_PLAN 抬头 |
| 待拍板(7 件,rooms / studio 侧):游戏房是否第七种房型、事件条算不算未读、房里的人能不能按上线、第二期换房、封面比例、谁能换封面、files 比上一版 | 未拍 | game_workroom.html ⑨ ⑬ |
待拍板(games 侧):hosted_bundles 底座把 games 三个指针列迁走 |
未拍,一拍 GAME_STAGING 分工表作废 | MINIAPP_PLATFORM §七.1 |
5. 出问题先查哪里 · 动它之前
排障一条路:
1. push 了手机还是旧的:先看后端日志有没有 publish_error(git 推成、发布没完成);再分线 —— 托管线看 entry_url 是不是 games.hoopcomm.com;再看指针(GET …/deploys/live)有没有动;没动 = 发号 / 就绪那步没走完(deploy.sh 只在 200 全绿后才 …/ready,409 = 别人抢先);动了还旧 = 缓存:.html 一律 60 秒优先、命中 live / staging 的 v{N} 非 html 一年(改版就是换路径,不该旧)、其余含 test/ 60 秒、lib/*.js 的边缘 immutable 副本要换地址。
2. 交活被拒:422 bad_path / bad_ext / too_large / bad_content / forbidden_content = 范围闸;422 no_changelog / stale_changelog = CHANGELOG 没动;422 no_base / 409 base_moved = 基线;409 race = GitHub 撞车;cloud:ci = git 已推、这版发布未完成、可能有残留对象。
3. 测试版看不到:不在那间房 → 后端静默回落正式版,不是报错;游戏没绑房 → 分身按不了 Build。
4. 对战被判负 / 没结算:看四个钟 —— 射钟 30 秒、心跳 12 秒、宽限 30 秒、结算等 4 秒;判负要「离线标记 + presence 不在线」两条;live 模式不判负。没结算 = 双方都没报 → 等 2 小时 TTL。卡是记分板但战绩里没有 = SaveMatch 失败(日志「落库战绩失败」),这条分支没有补偿,别处待核。
5. 约战卡没响:拉黑(两处)、ring_until 60 秒已过、对方 WS 在线就不发推送、被邀者正开着同一个游戏且在栈顶时不弹全局弹窗(交给游戏内)。
6. H5 说桥没有某方法:先分是桥(App 版本,跟 TestFlight)还是垫片 / 模拟器(hoop-games 仓);模拟器不是 1:1。
改它之前:
- 动版本 / 发布:发号必须先于写文件(deploy_race_test);别在任何脚本里抄「文件夹 → id」表;test/ 永远不许 immutable;清理永不删 Build。
- 动交活闸:目标文件夹只信 pages_folder;提示不许编造提交人;.glb 那类新格式要同时进白名单和魔数表。
- 动桥:JS 名和 Dart switch 一起改(守卫扫源码);GAME_DEV_KIT.md 是 SDK 唯一出处,PVP_ROOMS §5 那 9 个名字是假的。
- 动 hoop-kit:改 hoop-games 仓 lib/hoop-kit.js,跑 tools/sync_hoopkit.sh,别手改后端那份;不许加网络 / 导航(安检扫注释)。
- 动对战房:先读 PVP_ROOM_INTERNALS.md;seq / snapshot / turn_deadline 只由各自原子操作维护;新 live 游戏要在 liveGames 加一行;games.pvp 要手写 SQL。
- 别信的旧图纸:GAME_PLATFORM_SPEC 的状态与 SDK 清单;GAME_STAGING §一 那张「两条路」表;PVP_ROOMS §5 SDK 合约;PVP_GAME_INTEGRATION 少 7 个方法;GAME_WORKERS §二 第三闸叫错(html 那份的「验身闸(魔数)」才对);GAME_KIT 抬头「待部署」与 §7 自相矛盾、「九模块」实为十一;发布权的规矩以 Jeff 09-08 语音为准(主人 / 工作室管理员 / AI 三种),代码现状是 08-18 Vega 那版,两边差在 admin 那格。
6. 不能碰的数(当前值 / 实现出处 / 守卫或未覆盖)
🔒 = Jeff / 评审明确锁定的;其余是普通配置值,改之前把守卫一起改。
| 数 | 当前值 | 实现出处(af743ff8) | 守卫 / 未覆盖 |
|---|---|---|---|
| 一房一游戏 🔒 | 唯一索引 | 000206 | DB 约束 |
| 一人一房 🔒 | 进新房 = 旧房挂起 | room/ws.go:1044-1061 |
未覆盖 |
| 版本号只由后端发 🔒 | 发号先于写 | developer.go:1574;hoop-games deploy.sh:352-356 |
deploy_race_test.go |
| Build 永不删 🔒 | is_build 跳过清理 |
developer.go:1019-1053;deploy.sh:424-431 |
guard_keep_builds.sh(没接流水线) |
| 可访问 + 缓存 | 只放行 live / staging 的 v{N};html 60s 优先 / 命中 v{N} 非 html 一年 / 其余 60s / 封面 3600 | developer.go:961-1009、cover.go:49 |
未覆盖 |
| 留几版 | 5 | developer.go:1019 |
部分 |
| 探活 | 5 分钟、连续 2 次 | reconcile.go:114 |
未覆盖 |
| 日出口流量 | 10 GB / 游戏 | developer.go:209 |
未覆盖 |
| 交活上限 | 300 文件 / 20 MB;body 30 MiB;读文件 8 MiB | submit.go:42-43,100;tools_game.go:14 |
未覆盖 |
| 榜四道闸 🔒 | metric ^[a-z0-9_]{1,16}$、≤ 2^53、必须玩过、30 / 分 |
games/service.go:447-496 |
leaderboard_guard_test.go(含结构检查) |
| 影子 id | g_ + HMAC 24 位;token 15 分钟 |
identity.go:16-18,39-42 |
未覆盖 |
| 云存档 | 16 KB | save.go:22 |
未覆盖(hoop-games _sim_save_test.py 只守模拟器) |
| 「玩过」门槛 | 3 秒 | games/repository.go:259 |
mygames_test.go |
| 对战四个钟 🔒 | 射钟 30s / 心跳 12s / 宽限 30s / 结算等 4s | room/ws.go:13-15,33 |
间接(ws_flow_test.go、invite_gate_test.go) |
| live 时长 / 涨分上限 | 180000 ms;400 / 350 / 1200 / 800 每秒 | room/manager.go:81-102 |
live_test.go |
| 占座 = 呼叫窗 🔒 | 60 秒,同一常数引用 | room/manager.go:459、message/service.go:1807 |
invite_gate_test.go:63 |
| 房 TTL | 2 小时 | room/manager.go:48 |
未覆盖 |
| 限流 | say 10/s、action 20/s、create 15/min、invite 10/min、snapshot 2048 B | room/ws.go:18-26 |
未覆盖 |
| gamegate 缓存 | 60 秒 | room/gamegate.go:28 |
gamegate_test.go |
| 壳退避 | 1 / 3 / 6 秒 | game_load_error_view.dart:11-15 |
game_load_error_golden_test.dart |
| 壳心跳 | 5s / 12s / 8s / 900ms | game_webview_screen.dart:894-905 |
未覆盖 |
| 约战卡 | 268×150,最多 4 行 | game_invite_card.dart:188-189,472 |
golden gamecards_* |
_forceLegacySocial 🔒 |
必须为空 | game_webview_screen.dart:326 |
native_social_all_games_test.dart |
| errlog | 10 / 分 / IP,1 KB | errlog.go:25-27 |
未覆盖 |
| hoop-kit 版本 | 1.0.1 | developer/hoop-kit.js:27;hoop-games lib/hoop-kit.js:27 |
kit-test.html:63(hoop-games) |
7. 出处与旧图纸去向
- 代码(HOOP
[code@af743ff8]):backend/internal/domain/games/{model,handler,service,repository,identity,submit,gitfiles,contentgate,gallery,social,save,stats,records,analytics,ad,cover,errlog}.go(19 个_test.go)、backend/internal/domain/room/{manager,ws,gamegate,repository}.go、backend/internal/domain/developer/{developer,publish,reconcile,kit_manifest}.go+hoop-kit.js/hoop-compat.js、backend/internal/mcp/{tools_game,tools_work}.go、backend/internal/domain/message/service.go(约战卡)、Applib/screens/{game_webview_screen,games_hub_screen,game_stats_screen,game_analytics_screen,game_shop_sheet,ai_game_builder_screen,game_revenue_screen,game_file_reader_screen}.dart、lib/widgets/{game_invite_card,game_invite_listener,game_invite_picker,game_bottom_bar,game_switch_deck,game_back_button,game_load_error_view,game_profile_consent_sheet,game_sheet,active_match_bar_host,active_match_bar,game_social_pill}.dart、lib/util/{game_names,game_title,invite_ringtone}.dart。hoop-games[games@2f022816]:deploy.sh、guard_keep_builds.sh、_headers、_redirects、404.html、lib/{hoop-kit,hoop-sim,hoop-compat,pvp-live,social-bar}.js、.github/workflows/deploy.yml。 - 迁移:000055 games / game_players、000056 / 000057 样板、000059 icon、000121 matches、000130 shop、000133
game_invite类型、000134 画作、000136 社交、000139 云存档、000140live_version/game_deploys、000166 广告、000174 会话、000184 / 000201 / 000202 / 000212 / 000257 / 000336 任务卡(rooms)、000205 / 000206 staging 与绑房、000209 部署令牌、000234 note、000250cover_key(代码注释写 000247,不存在)、000260pages_folder、000274pvp、000285 草稿文件、000287 / 000338 中英名、000306 分享流、000315is_build、000316 一次性清版本、000333 邀请卡索引、000337source_hash。本模组最新 000338;仓库最新 000360。 - Redis:榜 ZSET、房间全部状态(
room:uroom:rinv:rres:rsnap:rsc:rdisc:rseen:rfinal:rheal:rsched:*)、gamegate 缓存、egress 计数、战绩册(stats.go零迁移);任务卡 / 交活不用 Redis。 - 生产:本文没有在生产机核任何项(49 个 active 是否真全在托管线、45 个陈旧指针现状、
liveGames名单、Caddy/hosted转发)→[prod@ 待核]。 - 决定(只列本文引到的):07-08 Jeff 一人一房;07-11 Jeff 足球「成长=个性」;07-14 Jeff 邀请卡六条;07-26 Jeff WebView 忽略本地缓存;08-05 Jeff kit A 案烘焙、源头在 hoop-games;08-11 Jeff 按一个按钮上线、房里人都能看测试版;08-12 Atlas 评审六条;08-13 Jeff 测试版按钮与正式版排在一起;08-15 Jeff 工人终局(零钥匙、三闸、代推、主令牌);08-18 Vega 发布权收紧、榜四道闸、身份归壳;08-21 Jeff 60 秒呼叫窗、hoop-compat;08-30 Jeff Build 永不清理;09-02 Lisa 全部版本化、live 0 = 不上平台;09-02 评审 #6 私房门禁;09-03 「交活只动这一个游戏」(署名冲突见下);09-03 Jeff「部署你以后不用问我」;09-04 Jeff
chrome({back:'own'})、中英不混;09-07 Vega Master.glb;09-07 賈斯特 返回回页内;09-08 Jeff(语音):交游戏 —— 任何 AI 都可以在游戏工作室里自己交到测试版和正式版,但一定要走我们的托管线;上线 —— 游戏主人、这个工作室的管理员、AI 三种都能按(测试版和正式版都算)。 - 未核清单:① 生产未核(见上);② 09-03 那句「改!」的署名不再追:Jeff 09-08 语音把规矩定成自己的(见决定);「现在游戏里 admin 按不了」是挡在后端
release_verdict还是 App 那层,未核;③ PVP_TEST_PLAN 的真机双端到底跑没跑;④liveGames名单和games.pvp=true的 19 个是否一致;⑤ hoop-games 仓五个不同的「游戏总数」(50 / 53 / 55 / 60 / 70)对应此刻 58 个index.html目录中哪些算游戏;⑥[games@…]标记本文检查器不核,行号靠分身;⑦ 交活写存储失败后 CI 是否会补跑:平台代推确实是PATCH refs/heads/master,deploy.yml也是 push master 触发,但触发凭据 / Secrets / 实际跑成未核;⑧ 拒绝同意后avatar_url指向的 persona 头像与真身头像是否同一张;⑨ 结算落库失败有没有别处补偿。 - 旧图纸去向:
GAME_PLATFORM_SPEC.md整份替代(四条铁律与影子身份公式并入 §0 / §3.5);GAME_KIT.md§1〜§9 替代(E2E 基准表留在原文供查),§10 工坊归 rooms / studio;GAME_WORKERS.md+game_workers.html合成本文 §1.1 / §3.3(html 独有的四条真虫与主令牌已并入),两份只留一份;PVP_GAME_INTEGRATION.md替代,骨架搬进GAME_DEV_KIT.md§3;GAME_STAGING.md§五 / §六 并入 §3.2,§一〜四留作历史并标废「两条路」表;PVP_ROOMS.md只留 §0b / §1 取向,§5 那 9 个名字要删;PVP_ROOM_INTERNALS.md保持独立(改代码看它),顺手修 §2rinv/rfinal各出现两次;GAME_DEV_KIT.md是 SDK 唯一出处,要改 §0 / §4 / §5 的「CF Pages + 找 Jeff 注册」为 publish-from-git、补.glb;GAME_INVITE_STANDARD.md§9 并入 §1.2;game_workroom.html③ ⑦ ⑬② 判据并入边界说明,⑪ ⑫ 归 AI_BOTS,⑬ 分刀表已全完成要标;PVP_TEST_PLAN.md留在原处、抬头要回填;SOCCER_SQUAD.md原样留,§2 改引本文;DEPLOY_PVP_20260708.md已归档不吸收;00-目录.md游戏段按:265约定该挂 ⚠️ 的至少五份。