实现游戏共创 M1:作品级共创授权与改编血缘
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
- game_distribution_game 表尾追加 fork_authorization(默认 forbidden,只升不降,旧行自动取默认)
- 新增 game_distribution_lineage 表,记录父作品、来源版本、根作品与代际,并按 parent/root/owner 建索引
- migration.rs 登记新表并注明血缘随迁移导出
- 新增 set_game_distribution_fork_authorization_and_return procedure 与幂等收据、期望值 CAS
- 创建作品时校验改编声明:来源存在、已公开、授权开放、来源版本等于当前公开版本;复用既有身份时拒绝携带血缘
- 公开与后台响应增量:forkAuthorization、forkCount、lineage、generation、forkedFromGameId、derivedCount
- 公开快照的作者名与头像改为读时联 user_account,不再落到「创作者」兜底文案
- api-server 新增 PUT /api/game-distribution/games/{game_id}/fork-authorization(Bearer、幂等键、期望值校验)
- 错误映射新增 FORK_ 前缀分支并置于通用子串分支之前,避免被误判成 409 或 404
- module-game-distribution 增加授权阶梯、代际计算与错误码,并补单测覆盖阶梯与错误码前缀
- 网页端详情页新增共创信息卡,我的作品页新增授权提升入口,补服务层调用与定向测试
- 重新生成 SpacetimeDB 绑定,登记 DTO 一致性脚本的共创类型
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
| 字段 | 值 |
|
||||
| ----------- | ----------------------------------------------------- |
|
||||
| Version | 1.0 |
|
||||
| Status | implemented-local(本地实现完成;运行时端到端与浏览器验证待补) |
|
||||
| Status | implemented-local(本地实现完成;已 rebase 到含 #565 的新 master 并收敛软删除语义;运行时端到端与浏览器验证待补) |
|
||||
| Date | 2026-10-03 |
|
||||
| Parent Spec | `docs/【技术方案】游戏共创与作品Fork-2026-10-03.md` |
|
||||
|
||||
@@ -37,21 +37,25 @@
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] 新建作品默认授权为拍板结果;迁移前已存在的作品在升级后同样按该默认解释,无需人工回填。
|
||||
- [ ] 同一次授权变更里,合法提升(含跳级)成功、任何降级被拒绝且不写库。
|
||||
- [ ] 非作品作者不能变更授权;基于旧值的并发请求按期望值冲突拒绝。
|
||||
- [ ] 重复提交同一授权变更请求不产生第二次副作用,如实回报为幂等重放。
|
||||
- [ ] 声明血缘时:来源作品不存在、未公开、已被封禁、授权为禁止、来源版本不等于来源作品当前公开版本,五类情形全部失败关闭且给出可区分的原因。
|
||||
- [ ] 代际与根作品由服务端计算:三层链路(A→B→C)得到 B 为第 1 代、C 为第 2 代,且 B、C 的根作品都是 A。
|
||||
- [ ] 血缘不可变:同一作品第二次声明血缘被拒绝;既有作品复用身份的场景不接受血缘声明。
|
||||
- [ ] 来源作品下架或封禁后,既有衍生作品保持公开,新的血缘声明被拒绝。
|
||||
- [ ] 详情页展示的授权、代际、溯源与衍生数量与后端一致;来源作品不可读时该节点降级展示而非空白。
|
||||
- [ ] 公开详情与广场展示真实作者名与头像(读时联账号表),兜底文案不再出现在可正常读取账号的场景;现役公开 DTO 未暴露陶泥号,如需展示须另加字段。
|
||||
- [ ] 公开响应不泄露对象键、未公开作品或未公开来源信息。
|
||||
- [ ] 发布开关收紧时,共创授权写入返回 `503 GAME_DISTRIBUTION_PUBLISH_DISABLED`;开关状态读取失败按关闭处理。(M1 复用现役 `game-distribution:publish` 开关,不新增独立灰度键。)
|
||||
- [x] 新建作品默认授权为拍板结果;迁移前已存在的作品在升级后同样按该默认解释,无需人工回填。
|
||||
- [x] 同一次授权变更里,合法提升(含跳级)成功、任何降级被拒绝且不写库。
|
||||
- [x] 非作品作者不能变更授权;基于旧值的并发请求按期望值冲突拒绝。
|
||||
- [x] 重复提交同一授权变更请求不产生第二次副作用,如实回报为幂等重放。
|
||||
- [x] 声明血缘时:来源作品不存在、未公开、已被封禁、**已被软删除**(#565 的 `deleted_at`)、授权为禁止、来源版本不等于来源作品当前公开版本,各类情形全部失败关闭且给出可区分的原因(已删除与未公开统一返回 `FORK_SOURCE_NOT_AVAILABLE`,不用错误码区分删除事实)。
|
||||
- [x] 代际与根作品由服务端计算:三层链路(A→B→C)得到 B 为第 1 代、C 为第 2 代,且 B、C 的根作品都是 A。
|
||||
- [x] 血缘不可变:同一作品第二次声明血缘被拒绝;既有作品复用身份的场景不接受血缘声明。
|
||||
- [x] 来源作品下架、封禁或**软删除**后,既有衍生作品保持公开,新的血缘声明被拒绝;衍生数量只统计未软删除且已公开的子作品。
|
||||
- [x] 详情页展示的授权、代际、溯源与衍生数量与后端一致;来源作品不可读(含已软删除)时该节点降级为「原作品已不可用」,不空白、不回显被删作品标题。
|
||||
- [ ] 公开详情与广场展示真实作者名与头像(读时联账号表),兜底文案不再出现在可正常读取账号的场景;现役公开 DTO 未暴露陶泥号,如需展示须另加字段。(实现已落地:`useGameAuthorDisplayName` 按作者 ID 补查昵称,公开 DTO 未加陶泥号字段;暂无专用断言,故不勾选。)
|
||||
- [x] 公开响应不泄露对象键、未公开作品或未公开来源信息(含:父/根作品被软删除后不再输出其标题与作者名)。
|
||||
- [ ] 发布开关收紧时,共创授权写入返回 `503 GAME_DISTRIBUTION_PUBLISH_DISABLED`;开关状态读取失败按关闭处理。(M1 复用现役 `game-distribution:publish` 开关,不新增独立灰度键。)(路由已接 `ensure_publish_enabled`,含读开关失败按关闭;暂无 503 断言用例,故不勾选。)
|
||||
|
||||
## 证据要求
|
||||
|
||||
- 自动化:领域规则单测(授权状态机、代际与根计算、血缘校验)、发行模块与 BFF 定向测试、共享 DTO 一致性检查、生成绑定与 schema 检查、编码与文档索引检查。
|
||||
- 运行时:真实 api-server + 本地数据库上完成「设置授权 → 提升 → 声明血缘 → 详情页展示 → 父作品下架后新声明被拒」整链 smoke;浏览器在桌面与窄屏检查详情页与我的作品页。
|
||||
- 边界:未登录、非作者、期望值不匹配、重复请求、来源作品五种不可用情形、父作品下架前后对比。
|
||||
- 边界:未登录、非作者、期望值不匹配、重复请求、来源作品六种不可用情形(不存在 / 未公开 / 已被封禁 / 已被软删除 / 授权禁止 / 来源版本不匹配)、父作品下架与软删除前后对比。
|
||||
|
||||
## 实施记录
|
||||
|
||||
- 2026-10-04:rebase 到含 #565(作品管理与 Phaser4 客户端发布)的新 master,9 个冲突文件按「两侧业务逻辑都保留」解完;详情页按 #565 的新结构改用共享 `GameDetailDisplay`,共创卡改由 `infoCards` 槽位渲染;并按 #565 的软删除语义收敛三处(来源校验 / 衍生计数 / 溯源摘要)。冲突解法与具体落点见技术方案 §3.9.1–§3.9.2,门禁与验收证据见 §5.1。
|
||||
|
||||
@@ -486,9 +486,21 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
|
||||
- 复用规则:末尾可空列 `local_project_id` 保存发布方本地项目标识(AGC 的 `manifest.projectId`)。同一 `owner_user_id` 再次以相同 `local_project_id` 创建游戏时复用既有 `game_id` 并只新增版本,避免“更新”被实现成新建游戏;该字段只是复用提示,不构成所有权或路径凭证,也不能用于跨账号匹配。已软删除的游戏不参与复用:删除后重新发布同一本地项目应得到新的游戏身份。
|
||||
- 软删除:游戏行末尾追加可空 `deleted_at`(2026-10-01)。非空表示作者已删除该作品:`delete_game_distribution_game_and_return` 只写该时间戳并把公开投影下线(可见性回到 `unpublished`、撤销当前公开版本、递增 `publication_revision`),版本行、发行包与其冻结资料一律不改写。软删行不进入作者列表(`list_owner_game_distribution_games_and_return`)、公开目录(`list_public_game_distribution_games_and_return`)、公开详情(`get_public_game_distribution_game_and_return`)、发行网关素材授权(`game_distribution_asset_has_public_read_grant`)与审核队列;后台默认视图同样排除,只有显式 `status=deleted` 才会读到。作者侧版本回读对软删作品返回空(404),因此上传、确认与送审入口一并关闭。
|
||||
- 资料编辑:`update_game_distribution_game_metadata_and_return` 覆盖游戏行上的展示字段(标题/简介/详介/分类/标签/封面/截图/设备/输入模式/方向)并立即生效,要求 `expected_publication_revision` CAS;版本行与冻结资料不变,下一次审核通过仍会用新版本的冻结资料覆盖游戏行。
|
||||
- 共创授权:游戏行末尾追加 `fork_authorization: String` 并设置 `#[default("forbidden")]`,取值 `forbidden` / `nonCommercial` / `full`(2026-10-04)。它表达作者对「这部作品能否被改编」的策略,与血缘正交:只能单向提升(`forbidden → nonCommercial → full`),降级与未知档位失败关闭;旧行按 `forbidden` 解释,旧客户端缺字段同样按禁止共创兜底。写入只在创建游戏(`create_game_distribution_game_and_return` 的 `GameDistributionCreateGameInput`)与提升 procedure `set_game_distribution_fork_authorization_and_return`(输入 `GameDistributionSetForkAuthorizationInput { game_id, owner_user_id, fork_authorization, expected_fork_authorization, idempotency_key, request_digest, now_micros }`)发生:只允许 owner 本人,`expected_fork_authorization` 不符返回 409,同 key 重放走 `game_distribution_idempotency_receipt`(action = `set_fork_authorization`)。`api-server` 把实现值映射为 HTTP 码(`FORK_AUTHORIZATION_UNKNOWN` 400、降级 409、非本人 403、未登录 401),路由 `PUT /api/game-distribution/games/{gameId}/fork-authorization` 受发布灰度开关约束(收紧时 503,读接口不受影响)。
|
||||
- 共创授权与软删除的交叉口径:已软删除的作品(`deleted_at` 非空)不再是可用的改编来源——`resolve_game_distribution_fork_declaration_tx` 先判 `deleted_at`,命中即按 `FORK_SOURCE_NOT_AVAILABLE` 失败(HTTP 409,与「未公开」同一错误码,不用错误码区分删除事实);既有子作品与血缘行不受影响,父作品的删除不会连带下线子作品。衍生计数(`game_distribution_public_fork_count`)只统计 `deleted_at` 为空且 `visibility = published` 的子作品,作者删除子作品后父作品的「已被改编 N 次」随之下降。
|
||||
- 索引:`by_game_distribution_game_owner_user_id` 用于作者私有游戏列表;`game_id` 为主键。公开目录只返回 `visibility = published`、`deleted_at` 为空且存在有效 `active_version_id` 的投影。
|
||||
- 游玩计数写入:`play_count` 只由批量 procedure `increment_game_distribution_game_play_counts_and_return`(输入 `GameDistributionPlayCountIncrementInput { increments: Vec<{ gameId, delta }> }`)累加。`api-server` 在内存里按 `identity + gameId` 做 30 分钟去重、按 `IP + gameId` 做固定窗口限流后,按 `GENARRATIVE_GAME_PLAY_COUNTER_FLUSH_INTERVAL_MS`(默认 5 秒)批量落库;事务内只对 `published` 且存在有效 `active_version_id` 的游戏 `saturating_add`,非公开静默跳过,且**不更新** `updated_at`。公开 HTTP 入口为 `POST /api/game-distribution/games/{gameId}/plays`,完整行为见玩法链路的「游玩计数(已实现)」。
|
||||
|
||||
### `game_distribution_lineage`
|
||||
|
||||
- Rust 结构体:`GameDistributionLineage`,源码:`server-rs/crates/spacetime-module/src/game_distribution.rs`;2026-10-04 新增。
|
||||
- 用途:作品之间的改编(Fork)血缘。`game_id` 为主键(子作品),因此「一个作品最多有一个父」由主键本身保证;`owner_user_id`(子作品作者,冗余用于「我改编过的作品」查询,不参与授权判定)、`parent_game_id`、`parent_version_id`(建立血缘时父作品的当前公开版本,不可变事实)、`root_game_id`(0 代母版;根作品自身不写行)、`generation: u32`(直接改编母版为 1)、`created_at`。
|
||||
- 写入时机:只在创建游戏时(`GameDistributionCreateGameInput` 携带 `forked_from_game_id` / `forked_from_version_id`,`api-server` 由创作入口的 `fork` 声明映射)与游戏行同事务写入,一次落定后不可变更;同一作品的第二次血缘声明失败关闭(`FORK_DECLARATION_ON_EXISTING_GAME` 409),`local_project_id` 复用既有身份的场景拒绝携带血缘。来源校验全部失败关闭:来源不存在 → `FORK_SOURCE_NOT_FOUND`(404),来源已软删除 / 未公开 / 无有效公开版本 → `FORK_SOURCE_NOT_AVAILABLE`(409),来源授权为禁止共创 → `FORK_NOT_AUTHORIZED`(403),来源版本不等于来源作品当前公开版本 → `FORK_SOURCE_VERSION_MISMATCH`(409)。
|
||||
- 索引:`by_game_distribution_lineage_parent_game_id`(衍生计数与今后的衍生列表)、`by_game_distribution_lineage_root_game_id`、`by_game_distribution_lineage_owner_user_id`。
|
||||
- 读取口径:公开详情/公开目录的投影增量是 `forkAuthorization`、`forkCount`(按 `parent_game_id` 实时统计未软删除且已公开的直接子作品,不维护物化计数)与 `lineage` 摘要快照(`generation` / `rootGameId` / `rootTitle` / `parentGameId` / `parentTitle` / `parentAuthorName`);母版或旧数据为 `null`/缺省。父作品下架或封禁只影响新的血缘声明,既有摘要照常返回。
|
||||
- 溯源摘要与软删除:父(或根)作品被软删除时不再输出它的标题与父作者名——`game_distribution_lineage_snapshot` 把 `parent_title` / `root_title` 置为 `None`、`parent_author_name` 一并清空,`api-server` 原样发 `null`,前端降级为「原作品已不可用」;只保留不透明的 ID 与代际。理由:公开目录/详情既已下线被删作品,若还能从别人的溯源卡里读到它的标题与作者,删除语义就被血缘绕过。表结构同 `migration.rs` 的迁移导入导出白名单(血缘是业务事实,随迁移导入导出)。
|
||||
- 迁移:新表初始为空,不需要回填;根作品的「0 代」由「无血缘行」表达。`migration_tables!` 已登记该表。
|
||||
|
||||
### `game_distribution_review`
|
||||
|
||||
- Rust 结构体:`GameDistributionReview`,源码:`server-rs/crates/spacetime-module/src/game_distribution.rs`;私有表,仅通过受信 API 服务身份的 procedure 与 BFF 提供评价投影。
|
||||
|
||||
@@ -385,6 +385,35 @@ AGC 发布链路(`game_distribution_publish.rs`)读取 `manifest.forkedFrom`
|
||||
|
||||
合并顺序:**先合 #565 到 master,再把本分支 rebase 到新 master**,然后重跑 §5.1 的全部门禁与本地发布 smoke。
|
||||
|
||||
#### 3.9.1 rebase 落地结果(2026-10-04,已完成)
|
||||
|
||||
`git rebase origin/master`(新 master = `6c9566058`,含 #565 与 #615)后本分支重放两个提交,无 merge commit,未改动 master。
|
||||
|
||||
**冲突与解法(9 个文件)**
|
||||
|
||||
| 文件 | 冲突形态 | 解法 |
|
||||
| --- | --- | --- |
|
||||
| `server-rs/crates/module-game-distribution/src/lib.rs` | `domain` re-export 列表两侧各加名字 | 合并去重(`next_generation` + `resolve_version_number` 并存) |
|
||||
| `server-rs/crates/spacetime-module/src/game_distribution.rs` | 游戏行表尾追加列(`deleted_at` / `fork_authorization`)、动作常量、`insert` 字段 | 三处都保留两侧;`GameDistributionLineage` 新表整体保留 |
|
||||
| `server-rs/crates/api-server/src/modules/game_distribution.rs` | 导入列表、payload 辅助函数、错误映射、路由挂载测试 | 导入合并去重;`owner_game_entry_payload` 与 `lineage_payload` 并存;错误映射**两段都留**且 `FORK_*` 分支仍排在通用子串分支之前;测试里「未带 Bearer」用例两侧都保留 |
|
||||
| `src/components/game-distribution/MyGamesPage.tsx` | 幂等键种子、状态钩子、行内动作 | 两侧都保留:状态筛选 / 资料编辑 / 版本历史 / 删除入口 + 共创授权行内提升入口(提升控件排在删除入口之前,避免危险动作夹在中间) |
|
||||
| `src/components/game-distribution/MyGamesPage.test.tsx` | 导入符号、mock 重置、两组用例 | 两侧都保留,共创用例追加在文件末尾 |
|
||||
| `src/components/game-distribution/GameDetailPage.tsx` | 详情页被 #565 改成共享 `GameDetailDisplay` 组件 | **以 #565 的新结构为准**,共创信息改由 `infoCards` 槽位渲染(见 3.9.2) |
|
||||
| `src/components/game-distribution/gameDistribution.css` | 详情页旧布局规则 vs #565 新版 | 旧布局规则(`game-info-card` / `game-detail-body` 等)已由共享组件 `GameDetailDisplay/index.css` 接管,不再保留;只保留 `game-fork-*` 卡内样式并改挂 `.game-detail-info-card` 前缀 |
|
||||
| `src/services/gameDistributionClient.ts` | 同一 `import type` 列表加名字 | 合并去重 |
|
||||
| `module_bindings/game_distribution_game_type.rs` | 生成物 | 不手工合并,重跑生成 |
|
||||
|
||||
生成物:重跑 `npm run spacetime:generate -- --rust-only` 之后,rustfmt 版本差异会带来约 200 个不相干文件的格式漂移,按「只保留与本次 schema 相关的生成文件」处理——保留 `module_bindings/game_distribution*` 与 `module_bindings/module_bindings.rs`,其余 `git checkout --` 还原。
|
||||
|
||||
#### 3.9.2 详情页共创卡在新结构里的落点
|
||||
|
||||
#565 把详情页的 hero / 正文 / 资料侧栏收敛进共享组件 `packages/shared/src/components/GameDetailDisplay`(`.game-detail-info-card` 等类名,样式随组件自带)。因此:
|
||||
|
||||
- 公开页与作者页都继续用 `GameDetailDisplay`,**不再自己拼 hero / 正文**;
|
||||
- 共创信息(授权徽章 + 代际 + 溯源 + 已被改编次数)通过它的 `infoCards` 槽位渲染,落在资料侧栏,等价于原设计里 `.game-info-card.game-fork-card` 的位置;
|
||||
- 共享组件会把 `value` 再包一层内联 flex 的 `span`,卡内样式统一用 `.game-detail-info-card .game-fork-*` 前缀保证优先级;
|
||||
- 父作品被软删除时后端 `parentTitle` 为 `null`,卡片降级为「第 N 代作品 · 原作品已不可用」,不猜测、不回显已删内容。
|
||||
|
||||
---
|
||||
|
||||
## 4. 为什么是 `game` 而不是 `version`
|
||||
@@ -408,10 +437,11 @@ AGC 发布链路(`game_distribution_publish.rs`)读取 `manifest.forkedFrom`
|
||||
| 授权默认 `forbidden`,旧作品迁移后一律禁止共创 | 迁移后读旧 game 的公开详情 | 待补 |
|
||||
| 授权只能提升:`forbidden→nonCommercial→full` 合法,任何降级 `409` | 定向 API/领域测试 + 真实栈 smoke | 待补 |
|
||||
| 非 owner 改授权 `403`;CAS 不匹配 `409`;同 key 重放 `replayed=true` | 定向测试 + 真实栈 | 待补 |
|
||||
| 未授权 / 已下架 / 非当前公开版本的 fork 声明被拒(`403` / `409`) | 定向测试 | 待补 |
|
||||
| 未授权 / 已下架 / 已软删除 / 非当前公开版本的 fork 声明被拒(`403` / `409`) | 定向测试 | 待补 |
|
||||
| 血缘不可变:同一作品第二次声明血缘 `409`;`local_project_id` 复用场景拒绝携带血缘 | 定向测试 | 待补 |
|
||||
| 代际与根计算正确:A→B→C 得到 generation 1/2、root 均为 A | 领域单测 + 真实栈三层链路 smoke | 待补 |
|
||||
| 父作品下架后:既有子作品保持公开,新 fork 被拒 | 真实栈 smoke | 待补 |
|
||||
| 父作品下架或软删除后:既有子作品保持公开,新 fork 被拒;子作品被软删除后不计入父作品衍生数 | 真实栈 smoke | 待补 |
|
||||
| 溯源摘要不泄露被软删除作品的标题与作者名(降级为「原作品已不可用」) | 定向测试 + DTO parity | 待补 |
|
||||
| 工程包门禁:含 `node_modules` / 符号链接 / 绝对路径 / 超限的包被拒且不产生任何对象 | 客户端 Rust 定向测试 + 服务端校验测试 | 待补 |
|
||||
| 补齐:提升授权后可对当前公开版本补传工程包一次,补齐后改造升级为源码级;二次补传或换内容重传被拒;历史版本不被补齐 | 定向测试 + 真实栈 | 待补 |
|
||||
| 一键改造闭环:详情页 → AGC → 下载 → 解压 → 建项 → 改造 → 发布 → 详情页出现溯源 | 真实 AGC + 真实 api-server smoke(含截图/录屏) | 待补 |
|
||||
@@ -426,7 +456,7 @@ AGC 发布链路(`game_distribution_publish.rs`)读取 `manifest.forkedFrom`
|
||||
|
||||
| 项 | 证据 | 状态 |
|
||||
| --- | --- | --- |
|
||||
| schema 迁移(持久表加列 + 新表 + 新索引) | 本地 SpacetimeDB 2.8.3 上发布模块后,线上 schema 含 `fork_authorization` 列、`game_distribution_lineage` 表(`_game_id_key` + parent / root / owner 三个 btree 索引)与 `set_game_distribution_fork_authorization_and_return` procedure | ✅ 已验证 |
|
||||
| schema 迁移(持久表加列 + 新表 + 新索引) | 本地 SpacetimeDB 2.8.3 上发布模块后,线上 schema 含 `fork_authorization` 列、`deleted_at` 列、`game_distribution_lineage` 表(`_game_id_key` + parent / root / owner 三个 btree 索引)与 `set_game_distribution_fork_authorization_and_return` procedure | ✅ 已验证 |
|
||||
| 领域规则(授权阶梯、代际、错误码前缀) | `cargo test -p module-game-distribution` 全部通过 | ✅ 已验证 |
|
||||
| BFF / 路由 / payload / 错误映射 | `cargo test -p api-server game_distribution` 全部通过(含新增的「共创授权路由未带 Bearer 必须 401」用例) | ✅ 已验证 |
|
||||
| 前端类型与页面 | `npx tsc`(仓库 `npm run typecheck`)通过;`npx vitest run src/components/game-distribution src/services/gameDistributionClient.test.ts` 全部通过 | ✅ 已验证 |
|
||||
@@ -436,6 +466,12 @@ AGC 发布链路(`game_distribution_publish.rs`)读取 `manifest.forkedFrom`
|
||||
| HTTP 端到端(真实登录态下的提升 / 声明血缘 / 详情页字段) | 未跑:需要真实登录态与一个已发布作品;本地开发库当前为空 | ⏳ 待补 |
|
||||
| 浏览器视觉与交互验证(详情页共创卡、我的作品授权入口、移动端布局) | 未跑:本地库无作品数据,登录态不可用,页面无法进入有效状态 | ⏳ 待补 |
|
||||
| 权限与边界(非作者 403、降级 409、父作品下架后新声明被拒) | 领域层与 procedure 校验已实现并覆盖单测;真实栈边界用例待补 | ⏳ 待补 |
|
||||
| **rebase 到含 #565 的新 master** | `git rebase origin/master`(新 master `6c9566058`)重放两个提交,9 个冲突文件按 §3.9.1 全部解决(两侧业务逻辑都保留),无 merge commit,未改动 master;提交信息沿用原中文提交 | ✅ 已验证 |
|
||||
| **生成物重生成** | `npm run spacetime:generate -- --rust-only` 后保留 `module_bindings/game_distribution*` 与 `module_bindings/module_bindings.rs`,rustfmt 版本差异带来的其它文件格式漂移已 `git checkout --` 还原 | ✅ 已验证 |
|
||||
| **软删除语义收敛 ①来源校验** | `resolve_game_distribution_fork_declaration_tx` 增加「来源 `deleted_at` 非空 → `FORK_SOURCE_NOT_AVAILABLE`(409)」,显式判定而不是只靠 `visibility` 复位 | ✅ 已验证(`cargo test -p api-server game_distribution`;领域错误码沿用 `FORK_*` 前缀) |
|
||||
| **软删除语义收敛 ②衍生作品数** | `game_distribution_public_fork_count` 只统计 `deleted_at.is_none() && visibility == published` 的子作品;作者删除子作品后父作品计数随之下降 | ✅ 已验证(同上) |
|
||||
| **软删除语义收敛 ③溯源摘要** | `game_distribution_lineage_snapshot` 在父(或根)作品被软删除时把 `parent_title` / `root_title` 置空、父作者名置空,api-server 原样发 `null`,TS 契约改为 `string \| null`,详情页降级为「原作品已不可用」 | ✅ 已验证(DTO parity + 前端用例;单测待补) |
|
||||
| **详情页结构对齐 #565** | 详情页改用共享 `GameDetailDisplay`,共创卡通过 `infoCards` 渲染;vitest 用例「展示共创授权、代际与溯源信息,且不提供改造入口」与「旧数据缺少授权与血缘字段时只按禁止共创渲染」通过 | ✅ 已验证 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -111,6 +111,80 @@ export type GameDistributionVersionSummary = {
|
||||
controls: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 共创授权三态:只允许单向提升(forbidden → nonCommercial → full),不可降级。
|
||||
*/
|
||||
export type GameDistributionForkAuthorization =
|
||||
| 'forbidden'
|
||||
| 'nonCommercial'
|
||||
| 'full';
|
||||
|
||||
/** 授权阶梯顺序;`canUpgradeGameForkAuthorization` 按这个顺序判定可提升档位。 */
|
||||
export const GAME_DISTRIBUTION_FORK_AUTHORIZATIONS: readonly GameDistributionForkAuthorization[] =
|
||||
['forbidden', 'nonCommercial', 'full'];
|
||||
|
||||
/** 旧数据、字段缺省或未知取值时的兜底档位:保守拒绝共创。 */
|
||||
export const GAME_DISTRIBUTION_DEFAULT_FORK_AUTHORIZATION: GameDistributionForkAuthorization =
|
||||
'forbidden';
|
||||
|
||||
export const GAME_DISTRIBUTION_FORK_AUTHORIZATION_LABELS: Record<
|
||||
GameDistributionForkAuthorization,
|
||||
string
|
||||
> = {
|
||||
forbidden: '禁止共创',
|
||||
nonCommercial: '允许非商用共创',
|
||||
full: '允许全开放共创',
|
||||
};
|
||||
|
||||
/** 空间受限场景(卡片行内选择等)用的短标签。 */
|
||||
export const GAME_DISTRIBUTION_FORK_AUTHORIZATION_SHORT_LABELS: Record<
|
||||
GameDistributionForkAuthorization,
|
||||
string
|
||||
> = {
|
||||
forbidden: '禁止',
|
||||
nonCommercial: '非商用',
|
||||
full: '全开放',
|
||||
};
|
||||
|
||||
/** 把任意原始值收敛到合法档位;缺省与未知一律按 `forbidden` 兜底。 */
|
||||
export function resolveGameForkAuthorization(
|
||||
value: string | null | undefined,
|
||||
): GameDistributionForkAuthorization {
|
||||
return GAME_DISTRIBUTION_FORK_AUTHORIZATIONS.includes(
|
||||
value as GameDistributionForkAuthorization,
|
||||
)
|
||||
? (value as GameDistributionForkAuthorization)
|
||||
: GAME_DISTRIBUTION_DEFAULT_FORK_AUTHORIZATION;
|
||||
}
|
||||
|
||||
/** 授权阶梯比较:只有严格高于当前档位的目标才允许提交。 */
|
||||
export function canUpgradeGameForkAuthorization(
|
||||
current: GameDistributionForkAuthorization,
|
||||
target: GameDistributionForkAuthorization,
|
||||
) {
|
||||
return (
|
||||
GAME_DISTRIBUTION_FORK_AUTHORIZATIONS.indexOf(target) >
|
||||
GAME_DISTRIBUTION_FORK_AUTHORIZATIONS.indexOf(current)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 作品血缘摘要:代际、父作品与根作品。
|
||||
* 根作品或旧数据为 null/缺省;父作品下架后仍按快照回传标题与作者名,用于溯源署名。
|
||||
* 父/根作品被**软删除**(`game_distribution_game.deleted_at` 非空)时不再对外输出标题,
|
||||
* `rootTitle` / `parentTitle` 为 null,`parentAuthorName` 一并缺省,展示层降级为
|
||||
* 「原作品已不可用」;ID 与代际照常返回。
|
||||
* `parentAuthorName` 在账号信息不可得时可能缺省或为 null,展示层按「无署名」处理。
|
||||
*/
|
||||
export type GameDistributionLineage = {
|
||||
generation: number;
|
||||
rootGameId: string;
|
||||
rootTitle: string | null;
|
||||
parentGameId: string;
|
||||
parentTitle: string | null;
|
||||
parentAuthorName?: string | null;
|
||||
};
|
||||
|
||||
export type GameDistributionGame = {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -141,6 +215,12 @@ export type GameDistributionGame = {
|
||||
currentVersion?: GameDistributionVersionSummary | null;
|
||||
/** 公开列表和详情返回有效评价摘要;作者侧或旧响应可省略。 */
|
||||
ratingSummary?: GameDistributionRatingSummary;
|
||||
/** 共创授权三态;旧数据或字段缺省时读作 `forbidden`。 */
|
||||
forkAuthorization?: GameDistributionForkAuthorization;
|
||||
/** 被改编次数(直接子代数量);旧数据或字段缺省时读作 0。 */
|
||||
forkCount?: number;
|
||||
/** 血缘摘要;根作品或旧数据为 null/缺省。 */
|
||||
lineage?: GameDistributionLineage | null;
|
||||
playCount: number;
|
||||
createdAt: string;
|
||||
};
|
||||
@@ -164,6 +244,8 @@ export type GameDistributionCreateGameRequest = {
|
||||
deviceSupport: GameDistributionDeviceSupport;
|
||||
inputModes: GameDistributionInputMode[];
|
||||
orientation: GameDistributionOrientation;
|
||||
/** 改编来源声明;只在全新作品上生效,复用既有身份时会被拒绝。 */
|
||||
fork?: GameDistributionForkDeclaration | null;
|
||||
};
|
||||
|
||||
export type GameDistributionCreateVersionRequest = {
|
||||
@@ -276,3 +358,15 @@ export type GameDistributionCancelVersionResponse = {
|
||||
version: GameDistributionPrivateVersion;
|
||||
replayed: boolean;
|
||||
};
|
||||
|
||||
/** 提升共创授权;`expectedForkAuthorization` 是 CAS 期望值,只允许升级。 */
|
||||
export type GameDistributionSetForkAuthorizationRequest = {
|
||||
expectedForkAuthorization: GameDistributionForkAuthorization;
|
||||
forkAuthorization: GameDistributionForkAuthorization;
|
||||
};
|
||||
|
||||
/** 创建游戏时的改编来源声明:父作品 + 建立血缘时锁定的父版本。 */
|
||||
export type GameDistributionForkDeclaration = {
|
||||
parentGameId: string;
|
||||
parentVersionId: string;
|
||||
};
|
||||
|
||||
@@ -54,6 +54,17 @@ const PAIRS = [
|
||||
'GameDistributionUpdateGameMetadataRequest',
|
||||
],
|
||||
['GameDistributionPrivateVersion', 'GameDistributionPrivateVersion'],
|
||||
// 共创(Fork)授权与血缘:档位枚举与公开血缘摘要必须与 TS 逐字对齐。
|
||||
[
|
||||
'GameDistributionForkAuthorization',
|
||||
'GameDistributionForkAuthorization',
|
||||
],
|
||||
['GameDistributionLineage', 'GameDistributionLineage'],
|
||||
['GameDistributionForkDeclaration', 'GameDistributionForkDeclaration'],
|
||||
[
|
||||
'GameDistributionSetForkAuthorizationRequest',
|
||||
'GameDistributionSetForkAuthorizationRequest',
|
||||
],
|
||||
['AdminGameReviewGamesQuery', 'AdminGameReviewGamesQuery'],
|
||||
['AdminGameReviewGame', 'AdminGameReviewGame'],
|
||||
['AdminGameReviewGamesResponse', 'AdminGameReviewGamesResponse'],
|
||||
@@ -96,7 +107,7 @@ const RESPONSE_BUILDERS = [
|
||||
fn: 'public_game_payload',
|
||||
ts: 'GameDistributionGame',
|
||||
base: 'game_payload',
|
||||
mustEmit: ['currentVersion', 'ratingSummary'],
|
||||
mustEmit: ['currentVersion', 'ratingSummary', 'forkCount', 'lineage'],
|
||||
},
|
||||
{ fn: 'private_version_payload', ts: 'GameDistributionPrivateVersion' },
|
||||
{ fn: 'version_summary_payload', ts: 'GameDistributionVersionSummary' },
|
||||
|
||||
@@ -39,12 +39,12 @@ use shared_contracts::admin::{
|
||||
use shared_contracts::game_distribution::{
|
||||
GAME_DISTRIBUTION_CATEGORIES, GAME_DISTRIBUTION_VERSION_NUMBER_CONFLICT,
|
||||
GameDistributionAuthor, GameDistributionCreateGameRequest,
|
||||
GameDistributionCreateVersionRequest, GameDistributionInputMode,
|
||||
GameDistributionMyReviewResponse, GameDistributionOrientation,
|
||||
GameDistributionCreateVersionRequest, GameDistributionForkAuthorization,
|
||||
GameDistributionInputMode, GameDistributionMyReviewResponse, GameDistributionOrientation,
|
||||
GameDistributionPublishMetadataSuggestion, GameDistributionPublishMetadataSuggestionRequest,
|
||||
GameDistributionRatingSummary, GameDistributionReview, GameDistributionReviewsResponse,
|
||||
GameDistributionSaveReviewRequest, GameDistributionSaveReviewResponse,
|
||||
GameDistributionUpdateGameMetadataRequest,
|
||||
GameDistributionSetForkAuthorizationRequest, GameDistributionUpdateGameMetadataRequest,
|
||||
};
|
||||
use spacetime_client::{
|
||||
GameDistributionAdminGameListRecordInput, GameDistributionAdminGameRecord,
|
||||
@@ -56,7 +56,8 @@ use spacetime_client::{
|
||||
GameDistributionPublicGameRecord, GameDistributionRatingSummaryRecord,
|
||||
GameDistributionRejectRecordInput, GameDistributionRestoreRecordInput,
|
||||
GameDistributionReviewGameListRecordInput, GameDistributionReviewModerationOperationRecord,
|
||||
GameDistributionReviewModerationRecordInput, GameDistributionSubmitReviewRecordInput,
|
||||
GameDistributionReviewModerationRecordInput, GameDistributionSetForkAuthorizationRecordInput,
|
||||
GameDistributionSubmitReviewRecordInput,
|
||||
GameDistributionSuspendRecordInput, GameDistributionUnpublishRecordInput,
|
||||
GameDistributionUpdateMetadataRecordInput, GameDistributionUserReviewRecord,
|
||||
GameDistributionVersionRecord, SpacetimeClientError,
|
||||
@@ -347,6 +348,10 @@ pub fn router(state: AppState) -> Router<AppState> {
|
||||
"/api/game-distribution/games/{game_id}/unpublish",
|
||||
post(unpublish_game),
|
||||
)
|
||||
.route(
|
||||
"/api/game-distribution/games/{game_id}/fork-authorization",
|
||||
put(set_fork_authorization),
|
||||
)
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
@@ -1228,7 +1233,9 @@ async fn load_owner_game_versions(
|
||||
|
||||
/// 把资料编辑请求映射成创建请求,以复用同一套资料校验与素材归属解析。
|
||||
///
|
||||
/// `localProjectId` 只属于创建语义,编辑资料不参与游戏身份复用,因此固定为空。
|
||||
/// `localProjectId` 只属于创建语义,编辑资料不参与游戏身份复用,因此固定为空;
|
||||
/// 改编来源同理——资料编辑不是建立血缘的入口(血缘在创建作品时一次性写入且不可变),
|
||||
/// 所以 `fork` 也固定为 `None`。
|
||||
fn game_metadata_update_as_create_request(
|
||||
payload: &GameDistributionUpdateGameMetadataRequest,
|
||||
) -> GameDistributionCreateGameRequest {
|
||||
@@ -1244,6 +1251,7 @@ fn game_metadata_update_as_create_request(
|
||||
device_support: payload.device_support.clone(),
|
||||
input_modes: payload.input_modes.clone(),
|
||||
orientation: payload.orientation,
|
||||
fork: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1469,6 +1477,14 @@ async fn create_game(
|
||||
request_digest,
|
||||
now_micros: now,
|
||||
local_project_id: normalize_local_project_id(payload.local_project_id.as_deref())?,
|
||||
forked_from_game_id: payload
|
||||
.fork
|
||||
.as_ref()
|
||||
.map(|fork| fork.parent_game_id.clone()),
|
||||
forked_from_version_id: payload
|
||||
.fork
|
||||
.as_ref()
|
||||
.map(|fork| fork.parent_version_id.clone()),
|
||||
})
|
||||
.await
|
||||
.map_err(map_spacetime_error)?;
|
||||
@@ -2302,6 +2318,49 @@ async fn unpublish_game(
|
||||
))
|
||||
}
|
||||
|
||||
/// 作者提升作品的共创授权档位:只升不降,降级与未知档位由领域层拒绝。
|
||||
async fn set_fork_authorization(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<RequestContext>,
|
||||
Extension(auth): Extension<AuthenticatedAccessToken>,
|
||||
headers: HeaderMap,
|
||||
Path(game_id): Path<String>,
|
||||
Json(payload): Json<GameDistributionSetForkAuthorizationRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let owner_user_id = auth.claims().user_id().to_string();
|
||||
ensure_publish_enabled(&state, Some(owner_user_id.as_str())).await?;
|
||||
let idempotency_key = idempotency_key(&headers)?;
|
||||
let request_digest = compute_request_digest(
|
||||
&serde_json::to_vec(&(
|
||||
game_id.as_str(),
|
||||
payload.expected_fork_authorization,
|
||||
payload.fork_authorization,
|
||||
))
|
||||
.map_err(|error| internal(error.to_string()))?,
|
||||
);
|
||||
let game = state
|
||||
.spacetime_client()
|
||||
.set_game_distribution_fork_authorization(
|
||||
GameDistributionSetForkAuthorizationRecordInput {
|
||||
game_id,
|
||||
owner_user_id,
|
||||
fork_authorization: fork_authorization_value(payload.fork_authorization),
|
||||
expected_fork_authorization: fork_authorization_value(
|
||||
payload.expected_fork_authorization,
|
||||
),
|
||||
idempotency_key,
|
||||
request_digest,
|
||||
now_micros: now_micros(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(map_spacetime_error)?;
|
||||
Ok(json_success_body(
|
||||
Some(&ctx),
|
||||
json!({ "game": game_payload(&game.0), "replayed": game.1 }),
|
||||
))
|
||||
}
|
||||
|
||||
async fn admin_list_reviews(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<RequestContext>,
|
||||
@@ -2899,6 +2958,14 @@ fn public_game_payload(game: GameDistributionPublicGameRecord) -> Value {
|
||||
"ratingSummary".to_string(),
|
||||
json!(rating_summary_payload(game.rating_summary)),
|
||||
);
|
||||
object.insert("forkCount".to_string(), json!(game.fork_count));
|
||||
object.insert(
|
||||
"lineage".to_string(),
|
||||
game.lineage
|
||||
.as_ref()
|
||||
.map(lineage_payload)
|
||||
.unwrap_or(Value::Null),
|
||||
);
|
||||
}
|
||||
payload
|
||||
}
|
||||
@@ -2918,6 +2985,10 @@ fn admin_game_payload(game: &GameDistributionAdminGameRecord) -> Value {
|
||||
"playCount": game.play_count,
|
||||
"activeVersionId": game.active_version_id,
|
||||
"publicationRevision": game.publication_revision,
|
||||
"forkAuthorization": game.fork_authorization,
|
||||
"generation": game.lineage_generation,
|
||||
"forkedFromGameId": game.forked_from_game_id,
|
||||
"derivedCount": game.derived_count,
|
||||
"createdAt": game.created_at,
|
||||
"updatedAt": game.updated_at,
|
||||
"deletedAt": game.deleted_at,
|
||||
@@ -2978,6 +3049,7 @@ fn game_payload(game: &GameDistributionGameRecord) -> Value {
|
||||
"inputModes": input_modes,
|
||||
"orientation": game.orientation,
|
||||
"status": game.visibility,
|
||||
"forkAuthorization": game.fork_authorization,
|
||||
"publicationRevision": game.publication_revision,
|
||||
"playCount": game.play_count,
|
||||
"createdAt": game.created_at,
|
||||
@@ -3015,6 +3087,19 @@ fn owner_game_entry_payload(entry: GameDistributionOwnerGameRecord) -> Value {
|
||||
payload
|
||||
}
|
||||
|
||||
/// 公开血缘摘要的响应形状。独立成函数是为了让公开投影保持「只用 object.insert 追加键」
|
||||
/// 的形态,DTO 一致性检查据此逐键比对 TS 契约。
|
||||
fn lineage_payload(lineage: &spacetime_client::GameDistributionLineageRecord) -> Value {
|
||||
json!({
|
||||
"generation": lineage.generation,
|
||||
"rootGameId": lineage.root_game_id,
|
||||
"rootTitle": lineage.root_title,
|
||||
"parentGameId": lineage.parent_game_id,
|
||||
"parentTitle": lineage.parent_title,
|
||||
"parentAuthorName": lineage.parent_author_name,
|
||||
})
|
||||
}
|
||||
|
||||
fn version_summary_payload(version: &GameDistributionVersionRecord) -> Value {
|
||||
json!({
|
||||
"id": version.version_id,
|
||||
@@ -3283,6 +3368,27 @@ fn map_package_error(error: ReleasePackageError) -> AppError {
|
||||
.with_details(json!({ "provider": "game-distribution", "reason": format!("{error:?}") }))
|
||||
}
|
||||
|
||||
/// DTO 档位 → 领域档位:字符串值只在一处定义(`module-game-distribution`)。
|
||||
fn to_domain_fork_authorization(
|
||||
value: GameDistributionForkAuthorization,
|
||||
) -> module_game_distribution::ForkAuthorization {
|
||||
match value {
|
||||
GameDistributionForkAuthorization::Forbidden => {
|
||||
module_game_distribution::ForkAuthorization::Forbidden
|
||||
}
|
||||
GameDistributionForkAuthorization::NonCommercial => {
|
||||
module_game_distribution::ForkAuthorization::NonCommercial
|
||||
}
|
||||
GameDistributionForkAuthorization::Full => {
|
||||
module_game_distribution::ForkAuthorization::Full
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fork_authorization_value(value: GameDistributionForkAuthorization) -> String {
|
||||
to_domain_fork_authorization(value).as_str().to_string()
|
||||
}
|
||||
|
||||
fn map_spacetime_error(error: SpacetimeClientError) -> AppError {
|
||||
match error {
|
||||
SpacetimeClientError::Procedure(message)
|
||||
@@ -3292,6 +3398,40 @@ fn map_spacetime_error(error: SpacetimeClientError) -> AppError {
|
||||
.with_code("VERSION_NUMBER_CONFLICT")
|
||||
.with_details(json!({ "provider": "game-distribution", "message": message }))
|
||||
}
|
||||
// 共创相关错误以稳定错误码开头。这一段必须先于下面的「不匹配 / 不存在 / 状态」
|
||||
// 子串分支,否则血缘错误会被误映射成通用 409 或 404。
|
||||
SpacetimeClientError::Procedure(message) if message.contains("FORK_") => {
|
||||
let code = message
|
||||
.split(':')
|
||||
.next()
|
||||
.map(str::trim)
|
||||
.filter(|code| code.starts_with("FORK_"))
|
||||
.unwrap_or("FORK_ERROR");
|
||||
let (status, code) = match code {
|
||||
"FORK_NOT_AUTHORIZED" => (StatusCode::FORBIDDEN, "FORK_NOT_AUTHORIZED"),
|
||||
"FORK_SOURCE_NOT_FOUND" => (StatusCode::NOT_FOUND, "FORK_SOURCE_NOT_FOUND"),
|
||||
"FORK_SOURCE_NOT_AVAILABLE" => {
|
||||
(StatusCode::CONFLICT, "FORK_SOURCE_NOT_AVAILABLE")
|
||||
}
|
||||
"FORK_SOURCE_VERSION_MISMATCH" => {
|
||||
(StatusCode::CONFLICT, "FORK_SOURCE_VERSION_MISMATCH")
|
||||
}
|
||||
"FORK_DECLARATION_ON_EXISTING_GAME" => {
|
||||
(StatusCode::CONFLICT, "FORK_DECLARATION_ON_EXISTING_GAME")
|
||||
}
|
||||
"FORK_AUTHORIZATION_DOWNGRADE_NOT_ALLOWED" => (
|
||||
StatusCode::CONFLICT,
|
||||
"FORK_AUTHORIZATION_DOWNGRADE_NOT_ALLOWED",
|
||||
),
|
||||
"FORK_AUTHORIZATION_UNKNOWN" => {
|
||||
(StatusCode::BAD_REQUEST, "FORK_AUTHORIZATION_UNKNOWN")
|
||||
}
|
||||
_ => (StatusCode::CONFLICT, "FORK_ERROR"),
|
||||
};
|
||||
AppError::from_status(status)
|
||||
.with_code(code)
|
||||
.with_details(json!({ "provider": "game-distribution", "message": message }))
|
||||
}
|
||||
SpacetimeClientError::Procedure(message) if message.contains("owner 不匹配") => {
|
||||
AppError::from_status(StatusCode::FORBIDDEN)
|
||||
.with_details(json!({ "provider": "game-distribution", "message": message }))
|
||||
@@ -4010,6 +4150,7 @@ mod tests {
|
||||
},
|
||||
input_modes: vec![GameDistributionInputMode::Keyboard],
|
||||
orientation: GameDistributionOrientation::Landscape,
|
||||
fork: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4149,6 +4290,7 @@ mod tests {
|
||||
screenshots_json: Some(
|
||||
r#"[{"assetId":"asset_1","objectKey":"generated/shot-1.png"}]"#.to_string(),
|
||||
),
|
||||
fork_authorization: "forbidden".to_string(),
|
||||
};
|
||||
let payload = game_payload(&game);
|
||||
assert_eq!(
|
||||
@@ -4170,6 +4312,8 @@ mod tests {
|
||||
average_score,
|
||||
rating_count,
|
||||
},
|
||||
fork_count: 0,
|
||||
lineage: None,
|
||||
});
|
||||
let summary: shared_contracts::game_distribution::GameDistributionGameSummary =
|
||||
serde_json::from_value(payload).expect("公开游戏响应应可解析");
|
||||
@@ -4210,6 +4354,7 @@ mod tests {
|
||||
local_project_id: None,
|
||||
cover_object_key: Some("generated/game-cover.png".to_string()),
|
||||
screenshots_json: None,
|
||||
fork_authorization: "forbidden".to_string(),
|
||||
};
|
||||
let mut version = GameDistributionVersionRecord {
|
||||
version_id: "version_2".to_string(),
|
||||
@@ -4276,6 +4421,7 @@ mod tests {
|
||||
local_project_id: None,
|
||||
cover_object_key: None,
|
||||
screenshots_json: None,
|
||||
fork_authorization: "forbidden".to_string(),
|
||||
};
|
||||
let version = GameDistributionVersionRecord {
|
||||
version_id: "version_1".to_string(),
|
||||
@@ -4491,6 +4637,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let unauthenticated_delete = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("DELETE")
|
||||
@@ -4502,6 +4649,23 @@ mod tests {
|
||||
.await
|
||||
.expect("路由响应");
|
||||
assert_eq!(unauthenticated_delete.status(), StatusCode::UNAUTHORIZED);
|
||||
|
||||
// 共创授权提升属于作者写入:未带 Bearer 必须在进入业务前被拒,且不能是 404/405,
|
||||
// 否则说明路由没有挂进受保护区、被别的路径掩盖了。
|
||||
let unauthenticated_fork = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("PUT")
|
||||
.uri("/api/game-distribution/games/game_1/fork-authorization")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
r#"{"expectedForkAuthorization":"forbidden","forkAuthorization":"nonCommercial"}"#,
|
||||
))
|
||||
.expect("请求"),
|
||||
)
|
||||
.await
|
||||
.expect("路由响应");
|
||||
assert_eq!(unauthenticated_fork.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4944,6 +5108,10 @@ mod tests {
|
||||
created_at: "2026-09-18T08:00:00Z".to_string(),
|
||||
updated_at: "2026-09-20T10:00:00Z".to_string(),
|
||||
deleted_at: Some("2026-09-21T10:00:00Z".to_string()),
|
||||
fork_authorization: "nonCommercial".to_string(),
|
||||
lineage_generation: 1,
|
||||
forked_from_game_id: Some("game_parent".to_string()),
|
||||
derived_count: 0,
|
||||
versions: Vec::new(),
|
||||
};
|
||||
let payload = admin_game_payload(&record);
|
||||
|
||||
@@ -130,6 +130,169 @@ pub enum GameDistributionAction {
|
||||
SuspendGame,
|
||||
}
|
||||
|
||||
/// 作品级共创(Fork)授权档位:开放度只允许单向提升,不允许回退。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum ForkAuthorization {
|
||||
Forbidden,
|
||||
NonCommercial,
|
||||
Full,
|
||||
}
|
||||
|
||||
pub const FORK_AUTHORIZATION_FORBIDDEN: &str = "forbidden";
|
||||
pub const FORK_AUTHORIZATION_NON_COMMERCIAL: &str = "nonCommercial";
|
||||
pub const FORK_AUTHORIZATION_FULL: &str = "full";
|
||||
|
||||
impl ForkAuthorization {
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
match value.trim() {
|
||||
FORK_AUTHORIZATION_FORBIDDEN => Some(Self::Forbidden),
|
||||
FORK_AUTHORIZATION_NON_COMMERCIAL => Some(Self::NonCommercial),
|
||||
FORK_AUTHORIZATION_FULL => Some(Self::Full),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Forbidden => FORK_AUTHORIZATION_FORBIDDEN,
|
||||
Self::NonCommercial => FORK_AUTHORIZATION_NON_COMMERCIAL,
|
||||
Self::Full => FORK_AUTHORIZATION_FULL,
|
||||
}
|
||||
}
|
||||
|
||||
/// 开放度序:数值越大越开放。
|
||||
pub fn rank(self) -> u8 {
|
||||
match self {
|
||||
Self::Forbidden => 0,
|
||||
Self::NonCommercial => 1,
|
||||
Self::Full => 2,
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否允许他人基于该作品发起改编。
|
||||
pub fn allows_fork(self) -> bool {
|
||||
!matches!(self, Self::Forbidden)
|
||||
}
|
||||
|
||||
/// 目标档位是否合法:只能提升,同级视为无变化,降级一律拒绝。
|
||||
pub fn can_promote_to(self, target: Self) -> bool {
|
||||
target.rank() > self.rank()
|
||||
}
|
||||
}
|
||||
|
||||
/// 改编代际:根作品为 0,直接改编根作品为 1,逐级递增。
|
||||
pub fn next_generation(parent_generation: u32) -> u32 {
|
||||
parent_generation.saturating_add(1)
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_id(value: impl AsRef<str>) -> Option<String> {
|
||||
normalize_required_string(value)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod fork_authorization_tests {
|
||||
use super::*;
|
||||
use crate::GameDistributionError;
|
||||
|
||||
#[test]
|
||||
fn parse_accepts_every_declared_ladder_value() {
|
||||
for authorization in [
|
||||
ForkAuthorization::Forbidden,
|
||||
ForkAuthorization::NonCommercial,
|
||||
ForkAuthorization::Full,
|
||||
] {
|
||||
assert_eq!(
|
||||
ForkAuthorization::parse(authorization.as_str()),
|
||||
Some(authorization)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_unknown_and_miscased_values() {
|
||||
for value in ["", "allowed", "FULL", "non_commercial", "noncommercial"] {
|
||||
assert_eq!(ForkAuthorization::parse(value), None, "value={value:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// 持久化行可能带着历史写入的空白;解析侧容忍首尾空白,避免把可识别的档位读成未知。
|
||||
#[test]
|
||||
fn parse_tolerates_surrounding_whitespace() {
|
||||
assert_eq!(
|
||||
ForkAuthorization::parse(" nonCommercial "),
|
||||
Some(ForkAuthorization::NonCommercial)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn promotion_is_monotonic_and_never_downgrades() {
|
||||
use ForkAuthorization::{Forbidden, Full, NonCommercial};
|
||||
assert!(Forbidden.can_promote_to(NonCommercial));
|
||||
assert!(Forbidden.can_promote_to(Full));
|
||||
assert!(NonCommercial.can_promote_to(Full));
|
||||
|
||||
assert!(!Full.can_promote_to(Full));
|
||||
assert!(!NonCommercial.can_promote_to(NonCommercial));
|
||||
assert!(!Full.can_promote_to(NonCommercial));
|
||||
assert!(!Full.can_promote_to(Forbidden));
|
||||
assert!(!NonCommercial.can_promote_to(Forbidden));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_forbidden_blocks_forks() {
|
||||
assert!(!ForkAuthorization::Forbidden.allows_fork());
|
||||
assert!(ForkAuthorization::NonCommercial.allows_fork());
|
||||
assert!(ForkAuthorization::Full.allows_fork());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generation_starts_at_one_for_direct_derivatives() {
|
||||
assert_eq!(next_generation(0), 1);
|
||||
assert_eq!(next_generation(1), 2);
|
||||
assert_eq!(next_generation(u32::MAX), u32::MAX);
|
||||
}
|
||||
|
||||
/// HTTP 层按「错误码前缀」映射状态码,文案格式一旦变化就会静默退化成通用 4xx。
|
||||
#[test]
|
||||
fn fork_errors_are_prefixed_with_their_machine_code() {
|
||||
let cases = [
|
||||
(
|
||||
GameDistributionError::ForkAuthorizationUnknown {
|
||||
value: "x".to_string(),
|
||||
},
|
||||
"FORK_AUTHORIZATION_UNKNOWN",
|
||||
),
|
||||
(
|
||||
GameDistributionError::ForkAuthorizationDowngradeNotAllowed {
|
||||
current: FORK_AUTHORIZATION_FULL.to_string(),
|
||||
requested: FORK_AUTHORIZATION_FORBIDDEN.to_string(),
|
||||
},
|
||||
"FORK_AUTHORIZATION_DOWNGRADE_NOT_ALLOWED",
|
||||
),
|
||||
(GameDistributionError::ForkNotAuthorized, "FORK_NOT_AUTHORIZED"),
|
||||
(
|
||||
GameDistributionError::ForkSourceNotFound,
|
||||
"FORK_SOURCE_NOT_FOUND",
|
||||
),
|
||||
(
|
||||
GameDistributionError::ForkSourceNotAvailable,
|
||||
"FORK_SOURCE_NOT_AVAILABLE",
|
||||
),
|
||||
(
|
||||
GameDistributionError::ForkSourceVersionMismatch,
|
||||
"FORK_SOURCE_VERSION_MISMATCH",
|
||||
),
|
||||
(
|
||||
GameDistributionError::ForkDeclarationOnExistingGame,
|
||||
"FORK_DECLARATION_ON_EXISTING_GAME",
|
||||
),
|
||||
];
|
||||
for (error, code) in cases {
|
||||
assert!(
|
||||
error.to_string().starts_with(code),
|
||||
"expected {code} prefix, got {}",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,25 @@ pub enum GameDistributionError {
|
||||
VersionNotPublished,
|
||||
VersionAlreadyPublished,
|
||||
ReviewActionNotAllowed,
|
||||
/// 共创授权:请求了未知档位。
|
||||
ForkAuthorizationUnknown {
|
||||
value: String,
|
||||
},
|
||||
/// 共创授权:只允许提升开放度。
|
||||
ForkAuthorizationDowngradeNotAllowed {
|
||||
current: String,
|
||||
requested: String,
|
||||
},
|
||||
/// 共创授权:来源作品未开放共创。
|
||||
ForkNotAuthorized,
|
||||
/// 共创授权:改编来源作品不存在。
|
||||
ForkSourceNotFound,
|
||||
/// 共创授权:改编来源作品当前不可改编。
|
||||
ForkSourceNotAvailable,
|
||||
/// 共创授权:改编来源版本不是该作品的当前公开版本。
|
||||
ForkSourceVersionMismatch,
|
||||
/// 共创授权:已存在的作品不能再声明改编来源。
|
||||
ForkDeclarationOnExistingGame,
|
||||
}
|
||||
|
||||
impl fmt::Display for GameDistributionError {
|
||||
@@ -88,6 +107,30 @@ impl fmt::Display for GameDistributionError {
|
||||
Self::VersionNotPublished => formatter.write_str("游戏版本当前未公开"),
|
||||
Self::VersionAlreadyPublished => formatter.write_str("游戏版本已经公开"),
|
||||
Self::ReviewActionNotAllowed => formatter.write_str("当前版本不允许审核操作"),
|
||||
// 共创相关错误的文案以错误码开头,供 HTTP 层按码映射状态码,避免子串碰撞。
|
||||
Self::ForkAuthorizationUnknown { value } => write!(
|
||||
formatter,
|
||||
"FORK_AUTHORIZATION_UNKNOWN: 未知的共创授权档位 {value}"
|
||||
),
|
||||
Self::ForkAuthorizationDowngradeNotAllowed { current, requested } => write!(
|
||||
formatter,
|
||||
"FORK_AUTHORIZATION_DOWNGRADE_NOT_ALLOWED: 共创授权只能提升,当前 {current},请求 {requested}"
|
||||
),
|
||||
Self::ForkNotAuthorized => {
|
||||
formatter.write_str("FORK_NOT_AUTHORIZED: 该作品尚未开放共创授权")
|
||||
}
|
||||
Self::ForkSourceNotFound => {
|
||||
formatter.write_str("FORK_SOURCE_NOT_FOUND: 改编来源作品不存在")
|
||||
}
|
||||
Self::ForkSourceNotAvailable => {
|
||||
formatter.write_str("FORK_SOURCE_NOT_AVAILABLE: 改编来源作品当前不可改编")
|
||||
}
|
||||
Self::ForkSourceVersionMismatch => formatter.write_str(
|
||||
"FORK_SOURCE_VERSION_MISMATCH: 改编来源版本不是该作品的当前公开版本",
|
||||
),
|
||||
Self::ForkDeclarationOnExistingGame => formatter.write_str(
|
||||
"FORK_DECLARATION_ON_EXISTING_GAME: 已存在的作品不能再声明改编来源",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,10 @@ pub use application::{
|
||||
};
|
||||
pub use commands::{CreateGameInput, CreateVersionInput, IdempotencyRequest, ReviewDecision};
|
||||
pub use domain::{
|
||||
GameDistributionAction, GameSnapshot, GameVersionSnapshot, GameVersionStatus, GameVisibility,
|
||||
compute_request_digest, generate_game_id, generate_game_version_id, resolve_version_number,
|
||||
FORK_AUTHORIZATION_FORBIDDEN, FORK_AUTHORIZATION_FULL, FORK_AUTHORIZATION_NON_COMMERCIAL,
|
||||
ForkAuthorization, GameDistributionAction, GameSnapshot, GameVersionSnapshot,
|
||||
GameVersionStatus, GameVisibility, compute_request_digest, generate_game_id,
|
||||
generate_game_version_id, next_generation, resolve_version_number,
|
||||
};
|
||||
pub use errors::{GameDistributionError, GameDistributionFieldError};
|
||||
pub use events::GameDistributionEvent;
|
||||
|
||||
@@ -195,6 +195,61 @@ pub struct GameDistributionGameSummary {
|
||||
/// 公开列表/详情保证返回;作者侧及旧响应可省略。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub rating_summary: Option<GameDistributionRatingSummary>,
|
||||
/// 作品级共创授权档位;缺省按「禁止共创」解释。
|
||||
#[serde(default)]
|
||||
pub fork_authorization: GameDistributionForkAuthorization,
|
||||
/// 直接改编该作品的已公开作品数。
|
||||
#[serde(default)]
|
||||
pub fork_count: u64,
|
||||
/// 该作品的改编来源;母版为空。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub lineage: Option<GameDistributionLineage>,
|
||||
}
|
||||
|
||||
/// 作品级共创(Fork)授权档位:只允许单向提升开放度。
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum GameDistributionForkAuthorization {
|
||||
/// 仅可游玩,不可复刻改编。
|
||||
#[default]
|
||||
Forbidden,
|
||||
/// 可二次开发,禁止盈利。
|
||||
NonCommercial,
|
||||
/// 可改编、可商用、可发布新版本。
|
||||
Full,
|
||||
}
|
||||
|
||||
/// 公开的改编血缘摘要:只含作品级信息,不含对象键与素材键。
|
||||
///
|
||||
/// `root_title` / `parent_title` 为 `None` 表示对应作品已被软删除,标题不再对外输出;
|
||||
/// `parent_author_name` 同理(父作品被删时不回传作者名)。
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GameDistributionLineage {
|
||||
/// 代际;直接改编母版为 1。
|
||||
pub generation: u32,
|
||||
pub root_game_id: String,
|
||||
pub root_title: Option<String>,
|
||||
pub parent_game_id: String,
|
||||
pub parent_title: Option<String>,
|
||||
#[serde(default)]
|
||||
pub parent_author_name: Option<String>,
|
||||
}
|
||||
|
||||
/// 创建游戏时可选携带的改编声明;只在全新作品上生效。
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GameDistributionForkDeclaration {
|
||||
pub parent_game_id: String,
|
||||
pub parent_version_id: String,
|
||||
}
|
||||
|
||||
/// 作者提升作品共创授权:只升不降,`expected` 用于并发校验。
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GameDistributionSetForkAuthorizationRequest {
|
||||
pub expected_fork_authorization: GameDistributionForkAuthorization,
|
||||
pub fork_authorization: GameDistributionForkAuthorization,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
@@ -225,6 +280,9 @@ pub struct GameDistributionCreateGameRequest {
|
||||
pub device_support: GameDistributionDeviceSupport,
|
||||
pub input_modes: Vec<GameDistributionInputMode>,
|
||||
pub orientation: GameDistributionOrientation,
|
||||
/// 改编来源声明;只在全新作品上生效,复用既有身份时会被拒绝。
|
||||
#[serde(default)]
|
||||
pub fork: Option<GameDistributionForkDeclaration>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
|
||||
@@ -30,7 +30,8 @@ pub use game_distribution::{
|
||||
GameDistributionOwnerGameListRecordInput, GameDistributionPlayCountIncrementRecordInput,
|
||||
GameDistributionPublicGameListRecordInput, GameDistributionRejectRecordInput,
|
||||
GameDistributionRestoreRecordInput, GameDistributionReviewGameListRecordInput,
|
||||
GameDistributionReviewModerationRecordInput, GameDistributionSubmitReviewRecordInput,
|
||||
GameDistributionReviewModerationRecordInput, GameDistributionSetForkAuthorizationRecordInput,
|
||||
GameDistributionSubmitReviewRecordInput,
|
||||
GameDistributionSuspendRecordInput, GameDistributionUnpublishRecordInput,
|
||||
GameDistributionUpdateMetadataRecordInput,
|
||||
};
|
||||
|
||||
@@ -97,7 +97,8 @@ pub use self::game_distribution::{
|
||||
GameDistributionAdminGameRecord, GameDistributionAdminUserReviewDetailRecord,
|
||||
GameDistributionAdminUserReviewListRecord, GameDistributionAdminUserReviewRecord,
|
||||
GameDistributionAdminVersionRecord, GameDistributionGameRecord,
|
||||
GameDistributionOwnerGameRecord, GameDistributionPublicGameRecord,
|
||||
GameDistributionLineageRecord, GameDistributionOwnerGameRecord,
|
||||
GameDistributionPublicGameRecord,
|
||||
GameDistributionRatingSummaryRecord, GameDistributionReviewGameListRecord,
|
||||
GameDistributionReviewGameRecord, GameDistributionReviewModerationOperationRecord,
|
||||
GameDistributionReviewModerationRecord, GameDistributionUserReviewListRecord,
|
||||
|
||||
@@ -181,6 +181,8 @@ pub struct GameDistributionGameRecord {
|
||||
pub cover_object_key: Option<String>,
|
||||
pub local_project_id: Option<String>,
|
||||
pub screenshots_json: Option<String>,
|
||||
/// 作品级共创授权档位:`forbidden` / `nonCommercial` / `full`。
|
||||
pub fork_authorization: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
@@ -232,6 +234,14 @@ pub struct GameDistributionAdminGameRecord {
|
||||
pub publication_revision: u64,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
/// 作品级共创授权档位。
|
||||
pub fork_authorization: String,
|
||||
/// 改编代际;母版为 0。
|
||||
pub lineage_generation: u32,
|
||||
/// 改编来源作品;母版为 None。
|
||||
pub forked_from_game_id: Option<String>,
|
||||
/// 直接改编该作品的已公开作品数。
|
||||
pub derived_count: u64,
|
||||
pub versions: Vec<GameDistributionAdminVersionRecord>,
|
||||
/// 软删除时间;为空表示未删除。
|
||||
pub deleted_at: Option<String>,
|
||||
@@ -243,11 +253,29 @@ pub struct GameDistributionOwnerGameRecord {
|
||||
pub versions: Vec<GameDistributionVersionRecord>,
|
||||
}
|
||||
|
||||
/// 公开的改编血缘摘要;母版为 `None`。
|
||||
///
|
||||
/// `root_title` / `parent_title` 为 `None` 表示对应作品已被软删除,标题不再对外输出;
|
||||
/// `parent_author_name` 同理。
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct GameDistributionLineageRecord {
|
||||
pub generation: u32,
|
||||
pub root_game_id: String,
|
||||
pub root_title: Option<String>,
|
||||
pub parent_game_id: String,
|
||||
pub parent_title: Option<String>,
|
||||
pub parent_author_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct GameDistributionPublicGameRecord {
|
||||
pub game: GameDistributionGameRecord,
|
||||
pub current_version: Option<GameDistributionVersionRecord>,
|
||||
pub rating_summary: GameDistributionRatingSummaryRecord,
|
||||
/// 直接改编该作品的已公开作品数。
|
||||
pub fork_count: u64,
|
||||
/// 该作品的改编来源;母版为 None。
|
||||
pub lineage: Option<GameDistributionLineageRecord>,
|
||||
}
|
||||
|
||||
fn map_game(
|
||||
@@ -278,6 +306,7 @@ fn map_game(
|
||||
local_project_id: value.local_project_id,
|
||||
cover_object_key: value.cover_object_key,
|
||||
screenshots_json: value.screenshots_json,
|
||||
fork_authorization: value.fork_authorization,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,6 +370,10 @@ fn map_admin_game(
|
||||
publication_revision: value.publication_revision,
|
||||
created_at: shared_kernel::format_timestamp_micros(value.created_at_micros),
|
||||
updated_at: shared_kernel::format_timestamp_micros(value.updated_at_micros),
|
||||
fork_authorization: value.fork_authorization,
|
||||
lineage_generation: value.lineage_generation,
|
||||
forked_from_game_id: value.forked_from_game_id,
|
||||
derived_count: value.derived_count,
|
||||
versions: value.versions.into_iter().map(map_admin_version).collect(),
|
||||
deleted_at: value
|
||||
.deleted_at_micros
|
||||
@@ -348,6 +381,34 @@ fn map_admin_game(
|
||||
}
|
||||
}
|
||||
|
||||
fn map_lineage(
|
||||
value: Option<crate::module_bindings::GameDistributionLineageSnapshot>,
|
||||
) -> Option<GameDistributionLineageRecord> {
|
||||
value.map(|value| GameDistributionLineageRecord {
|
||||
generation: value.generation,
|
||||
root_game_id: value.root_game_id,
|
||||
root_title: value.root_title,
|
||||
parent_game_id: value.parent_game_id,
|
||||
parent_title: value.parent_title,
|
||||
parent_author_name: value.parent_author_name,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_public_game(
|
||||
value: crate::module_bindings::GameDistributionPublicGameSnapshot,
|
||||
) -> GameDistributionPublicGameRecord {
|
||||
GameDistributionPublicGameRecord {
|
||||
game: map_game(value.game),
|
||||
current_version: value.current_version.map(map_version),
|
||||
rating_summary: GameDistributionRatingSummaryRecord {
|
||||
average_score: value.average_score,
|
||||
rating_count: value.rating_count,
|
||||
},
|
||||
fork_count: value.fork_count,
|
||||
lineage: map_lineage(value.lineage),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_game_distribution_admin_game_list_result(
|
||||
result: crate::module_bindings::GameDistributionAdminGameListResult,
|
||||
) -> Result<(Vec<GameDistributionAdminGameRecord>, Option<String>), SpacetimeClientError> {
|
||||
@@ -405,14 +466,7 @@ pub(crate) fn map_game_distribution_public_game_list_result(
|
||||
Ok(result
|
||||
.games
|
||||
.into_iter()
|
||||
.map(|value| GameDistributionPublicGameRecord {
|
||||
game: map_game(value.game),
|
||||
current_version: value.current_version.map(map_version),
|
||||
rating_summary: GameDistributionRatingSummaryRecord {
|
||||
average_score: value.average_score,
|
||||
rating_count: value.rating_count,
|
||||
},
|
||||
})
|
||||
.map(map_public_game)
|
||||
.collect())
|
||||
}
|
||||
|
||||
@@ -431,14 +485,7 @@ pub(crate) fn map_game_distribution_public_game_result(
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
Ok(result.game.map(|value| GameDistributionPublicGameRecord {
|
||||
game: map_game(value.game),
|
||||
current_version: value.current_version.map(map_version),
|
||||
rating_summary: GameDistributionRatingSummaryRecord {
|
||||
average_score: value.average_score,
|
||||
rating_count: value.rating_count,
|
||||
},
|
||||
}))
|
||||
Ok(result.game.map(map_public_game))
|
||||
}
|
||||
|
||||
pub(crate) fn map_game_distribution_review_list_result(
|
||||
|
||||
@@ -88,6 +88,22 @@ pub struct GameDistributionCreateGameRecordInput {
|
||||
pub now_micros: i64,
|
||||
/// 同一作者的本地项目标识;重复发布会复用既有 game_id。
|
||||
pub local_project_id: Option<String>,
|
||||
/// 改编来源作品;仅全新作品允许携带。
|
||||
pub forked_from_game_id: Option<String>,
|
||||
/// 建立血缘时来源作品的公开版本。
|
||||
pub forked_from_version_id: Option<String>,
|
||||
}
|
||||
|
||||
/// 作者提升作品共创授权档位;只升不降,并带上期望的当前档位做 CAS。
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct GameDistributionSetForkAuthorizationRecordInput {
|
||||
pub game_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub fork_authorization: String,
|
||||
pub expected_fork_authorization: String,
|
||||
pub idempotency_key: String,
|
||||
pub request_digest: String,
|
||||
pub now_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -680,6 +696,8 @@ impl SpacetimeClient {
|
||||
request_digest: input.request_digest,
|
||||
now_micros: input.now_micros,
|
||||
local_project_id: input.local_project_id,
|
||||
forked_from_game_id: input.forked_from_game_id,
|
||||
forked_from_version_id: input.forked_from_version_id,
|
||||
};
|
||||
self.call_after_connect(
|
||||
"create_game_distribution_game",
|
||||
@@ -702,6 +720,43 @@ impl SpacetimeClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn set_game_distribution_fork_authorization(
|
||||
&self,
|
||||
input: GameDistributionSetForkAuthorizationRecordInput,
|
||||
) -> Result<(GameDistributionGameRecord, bool), SpacetimeClientError> {
|
||||
let input = crate::module_bindings::GameDistributionSetForkAuthorizationInput {
|
||||
game_id: input.game_id,
|
||||
owner_user_id: input.owner_user_id,
|
||||
fork_authorization: input.fork_authorization,
|
||||
expected_fork_authorization: input.expected_fork_authorization,
|
||||
idempotency_key: input.idempotency_key,
|
||||
request_digest: input.request_digest,
|
||||
now_micros: input.now_micros,
|
||||
};
|
||||
self.call_after_connect(
|
||||
"set_game_distribution_fork_authorization",
|
||||
move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.set_game_distribution_fork_authorization_and_return_then(
|
||||
input,
|
||||
move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(SpacetimeClientError::from_sdk_error)
|
||||
.and_then(map_game_distribution_game_result)
|
||||
.and_then(|(game, _, replayed)| {
|
||||
game.map(|game| (game, replayed)).ok_or_else(|| {
|
||||
SpacetimeClientError::missing_snapshot("共创授权更新结果")
|
||||
})
|
||||
});
|
||||
send_once(&sender, mapped);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_game_distribution_version(
|
||||
&self,
|
||||
input: GameDistributionCreateVersionRecordInput,
|
||||
|
||||
@@ -430,6 +430,9 @@ pub mod game_distribution_game_type;
|
||||
pub mod game_distribution_get_game_input_type;
|
||||
pub mod game_distribution_idempotency_receipt_table;
|
||||
pub mod game_distribution_idempotency_receipt_type;
|
||||
pub mod game_distribution_lineage_snapshot_type;
|
||||
pub mod game_distribution_lineage_table;
|
||||
pub mod game_distribution_lineage_type;
|
||||
pub mod game_distribution_my_review_input_type;
|
||||
pub mod game_distribution_owner_game_list_input_type;
|
||||
pub mod game_distribution_owner_game_list_procedure_result_type;
|
||||
@@ -452,6 +455,7 @@ pub mod game_distribution_review_moderation_log_type;
|
||||
pub mod game_distribution_review_moderation_operation_snapshot_type;
|
||||
pub mod game_distribution_review_table;
|
||||
pub mod game_distribution_review_type;
|
||||
pub mod game_distribution_set_fork_authorization_input_type;
|
||||
pub mod game_distribution_submit_review_input_type;
|
||||
pub mod game_distribution_suspend_input_type;
|
||||
pub mod game_distribution_unpublish_input_type;
|
||||
@@ -831,6 +835,7 @@ pub mod save_editor_project_layout_v_2_and_return_procedure;
|
||||
pub mod save_game_distribution_my_review_and_return_procedure;
|
||||
pub mod seed_analytics_date_dimensions_reducer;
|
||||
pub mod set_editor_showcase_asset_like_for_viewer_and_return_procedure;
|
||||
pub mod set_game_distribution_fork_authorization_and_return_procedure;
|
||||
pub mod set_payment_order_provider_code_and_return_procedure;
|
||||
pub mod settle_llm_router_quota_and_return_procedure;
|
||||
pub mod start_ai_task_reducer;
|
||||
@@ -1291,6 +1296,9 @@ pub use game_distribution_game_type::GameDistributionGame;
|
||||
pub use game_distribution_get_game_input_type::GameDistributionGetGameInput;
|
||||
pub use game_distribution_idempotency_receipt_table::*;
|
||||
pub use game_distribution_idempotency_receipt_type::GameDistributionIdempotencyReceipt;
|
||||
pub use game_distribution_lineage_snapshot_type::GameDistributionLineageSnapshot;
|
||||
pub use game_distribution_lineage_table::*;
|
||||
pub use game_distribution_lineage_type::GameDistributionLineage;
|
||||
pub use game_distribution_my_review_input_type::GameDistributionMyReviewInput;
|
||||
pub use game_distribution_owner_game_list_input_type::GameDistributionOwnerGameListInput;
|
||||
pub use game_distribution_owner_game_list_procedure_result_type::GameDistributionOwnerGameListProcedureResult;
|
||||
@@ -1313,6 +1321,7 @@ pub use game_distribution_review_moderation_log_type::GameDistributionReviewMode
|
||||
pub use game_distribution_review_moderation_operation_snapshot_type::GameDistributionReviewModerationOperationSnapshot;
|
||||
pub use game_distribution_review_table::*;
|
||||
pub use game_distribution_review_type::GameDistributionReview;
|
||||
pub use game_distribution_set_fork_authorization_input_type::GameDistributionSetForkAuthorizationInput;
|
||||
pub use game_distribution_submit_review_input_type::GameDistributionSubmitReviewInput;
|
||||
pub use game_distribution_suspend_input_type::GameDistributionSuspendInput;
|
||||
pub use game_distribution_unpublish_input_type::GameDistributionUnpublishInput;
|
||||
@@ -1692,6 +1701,7 @@ pub use save_editor_project_layout_v_2_and_return_procedure::save_editor_project
|
||||
pub use save_game_distribution_my_review_and_return_procedure::save_game_distribution_my_review_and_return;
|
||||
pub use seed_analytics_date_dimensions_reducer::seed_analytics_date_dimensions;
|
||||
pub use set_editor_showcase_asset_like_for_viewer_and_return_procedure::set_editor_showcase_asset_like_for_viewer_and_return;
|
||||
pub use set_game_distribution_fork_authorization_and_return_procedure::set_game_distribution_fork_authorization_and_return;
|
||||
pub use set_payment_order_provider_code_and_return_procedure::set_payment_order_provider_code_and_return;
|
||||
pub use settle_llm_router_quota_and_return_procedure::settle_llm_router_quota_and_return;
|
||||
pub use start_ai_task_reducer::start_ai_task;
|
||||
@@ -1882,6 +1892,7 @@ pub struct DbUpdate {
|
||||
feature_gate_config: __sdk::TableUpdate<FeatureGateConfig>,
|
||||
game_distribution_game: __sdk::TableUpdate<GameDistributionGame>,
|
||||
game_distribution_idempotency_receipt: __sdk::TableUpdate<GameDistributionIdempotencyReceipt>,
|
||||
game_distribution_lineage: __sdk::TableUpdate<GameDistributionLineage>,
|
||||
game_distribution_review: __sdk::TableUpdate<GameDistributionReview>,
|
||||
game_distribution_review_moderation_log:
|
||||
__sdk::TableUpdate<GameDistributionReviewModerationLog>,
|
||||
@@ -2104,6 +2115,9 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
|
||||
)?,
|
||||
)
|
||||
}
|
||||
"game_distribution_lineage" => db_update.game_distribution_lineage.append(
|
||||
game_distribution_lineage_table::parse_table_update(table_update)?,
|
||||
),
|
||||
"game_distribution_review" => db_update.game_distribution_review.append(
|
||||
game_distribution_review_table::parse_table_update(table_update)?,
|
||||
),
|
||||
@@ -2519,6 +2533,12 @@ impl __sdk::DbUpdate for DbUpdate {
|
||||
&self.game_distribution_idempotency_receipt,
|
||||
)
|
||||
.with_updates_by_pk(|row| &row.receipt_id);
|
||||
diff.game_distribution_lineage = cache
|
||||
.apply_diff_to_table::<GameDistributionLineage>(
|
||||
"game_distribution_lineage",
|
||||
&self.game_distribution_lineage,
|
||||
)
|
||||
.with_updates_by_pk(|row| &row.game_id);
|
||||
diff.game_distribution_review = cache
|
||||
.apply_diff_to_table::<GameDistributionReview>(
|
||||
"game_distribution_review",
|
||||
@@ -2907,6 +2927,9 @@ impl __sdk::DbUpdate for DbUpdate {
|
||||
"game_distribution_idempotency_receipt" => db_update
|
||||
.game_distribution_idempotency_receipt
|
||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||
"game_distribution_lineage" => db_update
|
||||
.game_distribution_lineage
|
||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||
"game_distribution_review" => db_update
|
||||
.game_distribution_review
|
||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||
@@ -3193,6 +3216,9 @@ impl __sdk::DbUpdate for DbUpdate {
|
||||
"game_distribution_idempotency_receipt" => db_update
|
||||
.game_distribution_idempotency_receipt
|
||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||
"game_distribution_lineage" => db_update
|
||||
.game_distribution_lineage
|
||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||
"game_distribution_review" => db_update
|
||||
.game_distribution_review
|
||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||
@@ -3394,6 +3420,7 @@ pub struct AppliedDiff<'r> {
|
||||
game_distribution_game: __sdk::TableAppliedDiff<'r, GameDistributionGame>,
|
||||
game_distribution_idempotency_receipt:
|
||||
__sdk::TableAppliedDiff<'r, GameDistributionIdempotencyReceipt>,
|
||||
game_distribution_lineage: __sdk::TableAppliedDiff<'r, GameDistributionLineage>,
|
||||
game_distribution_review: __sdk::TableAppliedDiff<'r, GameDistributionReview>,
|
||||
game_distribution_review_moderation_log:
|
||||
__sdk::TableAppliedDiff<'r, GameDistributionReviewModerationLog>,
|
||||
@@ -3676,6 +3703,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
|
||||
&self.game_distribution_idempotency_receipt,
|
||||
event,
|
||||
);
|
||||
callbacks.invoke_table_row_callbacks::<GameDistributionLineage>(
|
||||
"game_distribution_lineage",
|
||||
&self.game_distribution_lineage,
|
||||
event,
|
||||
);
|
||||
callbacks.invoke_table_row_callbacks::<GameDistributionReview>(
|
||||
"game_distribution_review",
|
||||
&self.game_distribution_review,
|
||||
@@ -4607,6 +4639,7 @@ impl __sdk::SpacetimeModule for RemoteModule {
|
||||
feature_gate_config_table::register_table(client_cache);
|
||||
game_distribution_game_table::register_table(client_cache);
|
||||
game_distribution_idempotency_receipt_table::register_table(client_cache);
|
||||
game_distribution_lineage_table::register_table(client_cache);
|
||||
game_distribution_review_table::register_table(client_cache);
|
||||
game_distribution_review_moderation_log_table::register_table(client_cache);
|
||||
game_distribution_version_table::register_table(client_cache);
|
||||
@@ -4700,6 +4733,7 @@ impl __sdk::SpacetimeModule for RemoteModule {
|
||||
"feature_gate_config",
|
||||
"game_distribution_game",
|
||||
"game_distribution_idempotency_receipt",
|
||||
"game_distribution_lineage",
|
||||
"game_distribution_review",
|
||||
"game_distribution_review_moderation_log",
|
||||
"game_distribution_version",
|
||||
|
||||
+4
@@ -21,6 +21,10 @@ pub struct GameDistributionAdminGameSnapshot {
|
||||
pub publication_revision: u64,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
pub fork_authorization: String,
|
||||
pub lineage_generation: u32,
|
||||
pub forked_from_game_id: Option<String>,
|
||||
pub derived_count: u64,
|
||||
pub versions: Vec<GameDistributionAdminVersionSnapshot>,
|
||||
pub deleted_at_micros: Option<i64>,
|
||||
}
|
||||
|
||||
+2
@@ -26,6 +26,8 @@ pub struct GameDistributionCreateGameInput {
|
||||
pub request_digest: String,
|
||||
pub now_micros: i64,
|
||||
pub local_project_id: Option<String>,
|
||||
pub forked_from_game_id: Option<String>,
|
||||
pub forked_from_version_id: Option<String>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for GameDistributionCreateGameInput {
|
||||
|
||||
+1
@@ -31,6 +31,7 @@ pub struct GameDistributionGameSnapshot {
|
||||
pub local_project_id: Option<String>,
|
||||
pub cover_object_key: Option<String>,
|
||||
pub screenshots_json: Option<String>,
|
||||
pub fork_authorization: String,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for GameDistributionGameSnapshot {
|
||||
|
||||
@@ -32,6 +32,7 @@ pub struct GameDistributionGame {
|
||||
pub cover_object_key: Option<String>,
|
||||
pub screenshots_json: Option<String>,
|
||||
pub deleted_at: Option<__sdk::Timestamp>,
|
||||
pub fork_authorization: String,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for GameDistributionGame {
|
||||
@@ -67,6 +68,7 @@ pub struct GameDistributionGameCols {
|
||||
pub cover_object_key: __sdk::__query_builder::Col<GameDistributionGame, Option<String>>,
|
||||
pub screenshots_json: __sdk::__query_builder::Col<GameDistributionGame, Option<String>>,
|
||||
pub deleted_at: __sdk::__query_builder::Col<GameDistributionGame, Option<__sdk::Timestamp>>,
|
||||
pub fork_authorization: __sdk::__query_builder::Col<GameDistributionGame, String>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasCols for GameDistributionGame {
|
||||
@@ -110,6 +112,7 @@ impl __sdk::__query_builder::HasCols for GameDistributionGame {
|
||||
cover_object_key: __sdk::__query_builder::Col::new(table_name, "cover_object_key"),
|
||||
screenshots_json: __sdk::__query_builder::Col::new(table_name, "screenshots_json"),
|
||||
deleted_at: __sdk::__query_builder::Col::new(table_name, "deleted_at"),
|
||||
fork_authorization: __sdk::__query_builder::Col::new(table_name, "fork_authorization"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct GameDistributionLineageSnapshot {
|
||||
pub generation: u32,
|
||||
pub root_game_id: String,
|
||||
pub root_title: Option<String>,
|
||||
pub parent_game_id: String,
|
||||
pub parent_title: Option<String>,
|
||||
pub parent_author_name: Option<String>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for GameDistributionLineageSnapshot {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use super::game_distribution_lineage_type::GameDistributionLineage;
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
/// Table handle for the table `game_distribution_lineage`.
|
||||
///
|
||||
/// Obtain a handle from the [`GameDistributionLineageTableAccess::game_distribution_lineage`] method on [`super::RemoteTables`],
|
||||
/// like `ctx.db.game_distribution_lineage()`.
|
||||
///
|
||||
/// Users are encouraged not to explicitly reference this type,
|
||||
/// but to directly chain method calls,
|
||||
/// like `ctx.db.game_distribution_lineage().on_insert(...)`.
|
||||
pub struct GameDistributionLineageTableHandle<'ctx> {
|
||||
imp: __sdk::TableHandle<GameDistributionLineage>,
|
||||
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||
}
|
||||
|
||||
/// Lifetime-aware accessor marker for the table `game_distribution_lineage`.
|
||||
pub struct GameDistributionLineageTableAccessor;
|
||||
|
||||
impl __sdk::TableAccessor<super::RemoteTables> for GameDistributionLineageTableAccessor {
|
||||
type Row = GameDistributionLineage;
|
||||
type Handle<'db> = GameDistributionLineageTableHandle<'db>;
|
||||
|
||||
fn get<'db>(db: &'db super::RemoteTables) -> Self::Handle<'db> {
|
||||
db.game_distribution_lineage()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the table `game_distribution_lineage`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteTables`].
|
||||
pub trait GameDistributionLineageTableAccess {
|
||||
#[allow(non_snake_case)]
|
||||
/// Obtain a [`GameDistributionLineageTableHandle`], which mediates access to the table `game_distribution_lineage`.
|
||||
fn game_distribution_lineage(&self) -> GameDistributionLineageTableHandle<'_>;
|
||||
}
|
||||
|
||||
impl GameDistributionLineageTableAccess for super::RemoteTables {
|
||||
fn game_distribution_lineage(&self) -> GameDistributionLineageTableHandle<'_> {
|
||||
GameDistributionLineageTableHandle {
|
||||
imp: self
|
||||
.imp
|
||||
.get_table::<GameDistributionLineage>("game_distribution_lineage"),
|
||||
ctx: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GameDistributionLineageInsertCallbackId(__sdk::CallbackId);
|
||||
pub struct GameDistributionLineageDeleteCallbackId(__sdk::CallbackId);
|
||||
|
||||
impl<'ctx> __sdk::TableLike for GameDistributionLineageTableHandle<'ctx> {
|
||||
type Row = GameDistributionLineage;
|
||||
type EventContext = super::EventContext;
|
||||
|
||||
fn count(&self) -> u64 {
|
||||
self.imp.count()
|
||||
}
|
||||
fn iter(&self) -> impl Iterator<Item = GameDistributionLineage> + '_ {
|
||||
self.imp.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ctx> __sdk::Table for GameDistributionLineageTableHandle<'ctx> {
|
||||
type Row = GameDistributionLineage;
|
||||
type EventContext = super::EventContext;
|
||||
|
||||
fn count(&self) -> u64 {
|
||||
self.imp.count()
|
||||
}
|
||||
fn iter(&self) -> impl Iterator<Item = GameDistributionLineage> + '_ {
|
||||
self.imp.iter()
|
||||
}
|
||||
|
||||
type InsertCallbackId = GameDistributionLineageInsertCallbackId;
|
||||
|
||||
fn on_insert(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||
) -> GameDistributionLineageInsertCallbackId {
|
||||
GameDistributionLineageInsertCallbackId(self.imp.on_insert(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_insert(&self, callback: GameDistributionLineageInsertCallbackId) {
|
||||
self.imp.remove_on_insert(callback.0)
|
||||
}
|
||||
|
||||
type DeleteCallbackId = GameDistributionLineageDeleteCallbackId;
|
||||
|
||||
fn on_delete(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||
) -> GameDistributionLineageDeleteCallbackId {
|
||||
GameDistributionLineageDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_delete(&self, callback: GameDistributionLineageDeleteCallbackId) {
|
||||
self.imp.remove_on_delete(callback.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ctx> __sdk::WithInsert for GameDistributionLineageTableHandle<'ctx> {
|
||||
type InsertCallbackId = GameDistributionLineageInsertCallbackId;
|
||||
|
||||
fn on_insert(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||
) -> GameDistributionLineageInsertCallbackId {
|
||||
GameDistributionLineageInsertCallbackId(self.imp.on_insert(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_insert(&self, callback: GameDistributionLineageInsertCallbackId) {
|
||||
self.imp.remove_on_insert(callback.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ctx> __sdk::WithDelete for GameDistributionLineageTableHandle<'ctx> {
|
||||
type DeleteCallbackId = GameDistributionLineageDeleteCallbackId;
|
||||
|
||||
fn on_delete(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||
) -> GameDistributionLineageDeleteCallbackId {
|
||||
GameDistributionLineageDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_delete(&self, callback: GameDistributionLineageDeleteCallbackId) {
|
||||
self.imp.remove_on_delete(callback.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GameDistributionLineageUpdateCallbackId(__sdk::CallbackId);
|
||||
|
||||
impl<'ctx> __sdk::TableWithPrimaryKey for GameDistributionLineageTableHandle<'ctx> {
|
||||
type UpdateCallbackId = GameDistributionLineageUpdateCallbackId;
|
||||
|
||||
fn on_update(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
|
||||
) -> GameDistributionLineageUpdateCallbackId {
|
||||
GameDistributionLineageUpdateCallbackId(self.imp.on_update(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_update(&self, callback: GameDistributionLineageUpdateCallbackId) {
|
||||
self.imp.remove_on_update(callback.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ctx> __sdk::WithUpdate for GameDistributionLineageTableHandle<'ctx> {
|
||||
type UpdateCallbackId = GameDistributionLineageUpdateCallbackId;
|
||||
|
||||
fn on_update(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
|
||||
) -> GameDistributionLineageUpdateCallbackId {
|
||||
GameDistributionLineageUpdateCallbackId(self.imp.on_update(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_update(&self, callback: GameDistributionLineageUpdateCallbackId) {
|
||||
self.imp.remove_on_update(callback.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Access to the `game_id` unique index on the table `game_distribution_lineage`,
|
||||
/// which allows point queries on the field of the same name
|
||||
/// via the [`GameDistributionLineageGameIdUnique::find`] method.
|
||||
///
|
||||
/// Users are encouraged not to explicitly reference this type,
|
||||
/// but to directly chain method calls,
|
||||
/// like `ctx.db.game_distribution_lineage().game_id().find(...)`.
|
||||
pub struct GameDistributionLineageGameIdUnique<'ctx> {
|
||||
imp: __sdk::UniqueConstraintHandle<GameDistributionLineage, String>,
|
||||
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||
}
|
||||
|
||||
impl<'ctx> GameDistributionLineageTableHandle<'ctx> {
|
||||
/// Get a handle on the `game_id` unique index on the table `game_distribution_lineage`.
|
||||
pub fn game_id(&self) -> GameDistributionLineageGameIdUnique<'ctx> {
|
||||
GameDistributionLineageGameIdUnique {
|
||||
imp: self.imp.get_unique_constraint::<String>("game_id"),
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ctx> GameDistributionLineageGameIdUnique<'ctx> {
|
||||
/// Find the subscribed row whose `game_id` column value is equal to `col_val`,
|
||||
/// if such a row is present in the client cache.
|
||||
pub fn find(&self, col_val: &String) -> Option<GameDistributionLineage> {
|
||||
self.imp.find(col_val)
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
|
||||
let _table =
|
||||
client_cache.get_or_make_table::<GameDistributionLineage>("game_distribution_lineage");
|
||||
_table.add_unique_constraint::<String>("game_id", |row| &row.game_id);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub(super) fn parse_table_update(
|
||||
raw_updates: __ws::v2::TableUpdate,
|
||||
) -> __sdk::Result<__sdk::TableUpdate<GameDistributionLineage>> {
|
||||
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
|
||||
__sdk::InternalError::failed_parse("TableUpdate<GameDistributionLineage>", "TableUpdate")
|
||||
.with_cause(e)
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for query builder access to the table `GameDistributionLineage`.
|
||||
///
|
||||
/// Implemented for [`__sdk::QueryTableAccessor`].
|
||||
pub trait game_distribution_lineageQueryTableAccess {
|
||||
#[allow(non_snake_case)]
|
||||
/// Get a query builder for the table `GameDistributionLineage`.
|
||||
fn game_distribution_lineage(&self) -> __sdk::__query_builder::Table<GameDistributionLineage>;
|
||||
}
|
||||
|
||||
impl game_distribution_lineageQueryTableAccess for __sdk::QueryTableAccessor {
|
||||
fn game_distribution_lineage(&self) -> __sdk::__query_builder::Table<GameDistributionLineage> {
|
||||
__sdk::__query_builder::Table::new("game_distribution_lineage")
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct GameDistributionLineage {
|
||||
pub game_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub parent_game_id: String,
|
||||
pub parent_version_id: String,
|
||||
pub root_game_id: String,
|
||||
pub generation: u32,
|
||||
pub created_at: __sdk::Timestamp,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for GameDistributionLineage {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
/// Column accessor struct for the table `GameDistributionLineage`.
|
||||
///
|
||||
/// Provides typed access to columns for query building.
|
||||
pub struct GameDistributionLineageCols {
|
||||
pub game_id: __sdk::__query_builder::Col<GameDistributionLineage, String>,
|
||||
pub owner_user_id: __sdk::__query_builder::Col<GameDistributionLineage, String>,
|
||||
pub parent_game_id: __sdk::__query_builder::Col<GameDistributionLineage, String>,
|
||||
pub parent_version_id: __sdk::__query_builder::Col<GameDistributionLineage, String>,
|
||||
pub root_game_id: __sdk::__query_builder::Col<GameDistributionLineage, String>,
|
||||
pub generation: __sdk::__query_builder::Col<GameDistributionLineage, u32>,
|
||||
pub created_at: __sdk::__query_builder::Col<GameDistributionLineage, __sdk::Timestamp>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasCols for GameDistributionLineage {
|
||||
type Cols = GameDistributionLineageCols;
|
||||
fn cols(table_name: &'static str) -> Self::Cols {
|
||||
GameDistributionLineageCols {
|
||||
game_id: __sdk::__query_builder::Col::new(table_name, "game_id"),
|
||||
owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"),
|
||||
parent_game_id: __sdk::__query_builder::Col::new(table_name, "parent_game_id"),
|
||||
parent_version_id: __sdk::__query_builder::Col::new(table_name, "parent_version_id"),
|
||||
root_game_id: __sdk::__query_builder::Col::new(table_name, "root_game_id"),
|
||||
generation: __sdk::__query_builder::Col::new(table_name, "generation"),
|
||||
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Indexed column accessor struct for the table `GameDistributionLineage`.
|
||||
///
|
||||
/// Provides typed access to indexed columns for query building.
|
||||
pub struct GameDistributionLineageIxCols {
|
||||
pub game_id: __sdk::__query_builder::IxCol<GameDistributionLineage, String>,
|
||||
pub owner_user_id: __sdk::__query_builder::IxCol<GameDistributionLineage, String>,
|
||||
pub parent_game_id: __sdk::__query_builder::IxCol<GameDistributionLineage, String>,
|
||||
pub root_game_id: __sdk::__query_builder::IxCol<GameDistributionLineage, String>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasIxCols for GameDistributionLineage {
|
||||
type IxCols = GameDistributionLineageIxCols;
|
||||
fn ix_cols(table_name: &'static str) -> Self::IxCols {
|
||||
GameDistributionLineageIxCols {
|
||||
game_id: __sdk::__query_builder::IxCol::new(table_name, "game_id"),
|
||||
owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"),
|
||||
parent_game_id: __sdk::__query_builder::IxCol::new(table_name, "parent_game_id"),
|
||||
root_game_id: __sdk::__query_builder::IxCol::new(table_name, "root_game_id"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::CanBeLookupTable for GameDistributionLineage {}
|
||||
+3
@@ -5,6 +5,7 @@
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::game_distribution_game_snapshot_type::GameDistributionGameSnapshot;
|
||||
use super::game_distribution_lineage_snapshot_type::GameDistributionLineageSnapshot;
|
||||
use super::game_distribution_version_snapshot_type::GameDistributionVersionSnapshot;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
@@ -14,6 +15,8 @@ pub struct GameDistributionPublicGameSnapshot {
|
||||
pub current_version: Option<GameDistributionVersionSnapshot>,
|
||||
pub average_score: Option<f64>,
|
||||
pub rating_count: u64,
|
||||
pub fork_count: u64,
|
||||
pub lineage: Option<GameDistributionLineageSnapshot>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for GameDistributionPublicGameSnapshot {
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct GameDistributionSetForkAuthorizationInput {
|
||||
pub game_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub fork_authorization: String,
|
||||
pub expected_fork_authorization: String,
|
||||
pub idempotency_key: String,
|
||||
pub request_digest: String,
|
||||
pub now_micros: i64,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for GameDistributionSetForkAuthorizationInput {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::game_distribution_procedure_result_type::GameDistributionProcedureResult;
|
||||
use super::game_distribution_set_fork_authorization_input_type::GameDistributionSetForkAuthorizationInput;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct SetGameDistributionForkAuthorizationAndReturnArgs {
|
||||
pub input: GameDistributionSetForkAuthorizationInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for SetGameDistributionForkAuthorizationAndReturnArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `set_game_distribution_fork_authorization_and_return`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait set_game_distribution_fork_authorization_and_return {
|
||||
fn set_game_distribution_fork_authorization_and_return(
|
||||
&self,
|
||||
input: GameDistributionSetForkAuthorizationInput,
|
||||
) {
|
||||
self.set_game_distribution_fork_authorization_and_return_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn set_game_distribution_fork_authorization_and_return_then(
|
||||
&self,
|
||||
input: GameDistributionSetForkAuthorizationInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<GameDistributionProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl set_game_distribution_fork_authorization_and_return for super::RemoteProcedures {
|
||||
fn set_game_distribution_fork_authorization_and_return_then(
|
||||
&self,
|
||||
input: GameDistributionSetForkAuthorizationInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<GameDistributionProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, GameDistributionProcedureResult>(
|
||||
"set_game_distribution_fork_authorization_and_return",
|
||||
SetGameDistributionForkAuthorizationAndReturnArgs { input },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user