From 1d7ef7e4b657a2d0f01f6179bd0a9cd6f03f1774 Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 14 May 2026 18:20:46 +0800 Subject: [PATCH 1/2] feat: wire bark battle platform loop --- ...K_BATTLE_PHASE2_PLATFORM_WORK_LOOP_PLAN.md | 549 +++++++++++ CONTEXT.md | 110 +++ docs/prd/BARK_BATTLE_BDD_2026-05-11.md | 123 ++- ...E_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md | 80 +- .../shared/src/contracts/barkBattle.test.ts | 130 +++ packages/shared/src/contracts/barkBattle.ts | 218 +++++ packages/shared/src/contracts/index.ts | 1 + server-rs/Cargo.lock | 11 + server-rs/Cargo.toml | 2 + server-rs/crates/api-server/Cargo.toml | 1 + server-rs/crates/api-server/src/app.rs | 46 + .../crates/api-server/src/bark_battle.rs | 776 ++++++++++++++++ .../api-server/src/creation_entry_config.rs | 8 + server-rs/crates/api-server/src/main.rs | 1 + server-rs/crates/api-server/src/puzzle.rs | 15 +- .../crates/module-bark-battle/Cargo.toml | 14 + .../crates/module-bark-battle/src/domain.rs | 162 ++++ .../crates/module-bark-battle/src/lib.rs | 5 + .../crates/module-bark-battle/src/scoring.rs | 316 +++++++ .../shared-contracts/src/bark_battle.rs | 497 ++++++++++ server-rs/crates/shared-contracts/src/lib.rs | 1 + .../spacetime-client/src/bark_battle.rs | 138 +++ server-rs/crates/spacetime-client/src/lib.rs | 46 +- .../crates/spacetime-client/src/mapper.rs | 36 + .../bark_battle_draft_config_row_type.rs | 86 ++ .../bark_battle_draft_config_table.rs | 162 ++++ ...k_battle_draft_config_upsert_input_type.rs | 23 + .../bark_battle_draft_create_input_type.rs | 26 + .../bark_battle_leaderboard_entry_row_type.rs | 94 ++ .../bark_battle_leaderboard_entry_table.rs | 171 ++++ ...attle_personal_best_projection_row_type.rs | 107 +++ ...k_battle_personal_best_projection_table.rs | 174 ++++ .../bark_battle_procedure_result_type.rs | 17 + .../bark_battle_published_config_row_type.rs | 90 ++ .../bark_battle_published_config_table.rs | 169 ++++ .../bark_battle_run_finish_input_type.rs | 32 + .../bark_battle_run_get_input_type.rs | 16 + .../bark_battle_run_start_input_type.rs | 23 + ...rk_battle_runtime_config_get_input_type.rs | 16 + .../bark_battle_runtime_run_row_type.rs | 127 +++ .../bark_battle_runtime_run_table.rs | 162 ++++ .../bark_battle_score_record_row_type.rs | 106 +++ .../bark_battle_score_record_table.rs | 162 ++++ .../bark_battle_work_publish_input_type.rs | 19 + ...k_battle_work_stats_projection_row_type.rs | 111 +++ ...bark_battle_work_stats_projection_table.rs | 169 ++++ .../create_bark_battle_draft_procedure.rs | 59 ++ .../finish_bark_battle_run_procedure.rs | 59 ++ .../get_bark_battle_run_procedure.rs | 59 ++ ...et_bark_battle_runtime_config_procedure.rs | 59 ++ .../src/module_bindings/mod.rs | 236 ++++- .../publish_bark_battle_work_procedure.rs | 59 ++ .../start_bark_battle_run_procedure.rs | 59 ++ ...date_bark_battle_draft_config_procedure.rs | 59 ++ server-rs/crates/spacetime-module/Cargo.toml | 2 + .../spacetime-module/src/bark_battle/mod.rs | 872 ++++++++++++++++++ .../src/bark_battle/tables.rs | 172 ++++ .../spacetime-module/src/bark_battle/types.rs | 177 ++++ server-rs/crates/spacetime-module/src/lib.rs | 2 + .../crates/spacetime-module/src/migration.rs | 12 + .../spacetime-module/src/runtime/mod.rs | 4 +- .../BarkBattleConfigEditor.test.tsx | 50 + .../BarkBattleConfigEditor.tsx | 161 ++++ .../BarkBattlePreviewCard.tsx | 56 ++ .../PlatformEntryFlowShellImpl.tsx | 118 +++ .../platform-entry/platformEntryTypes.ts | 2 + .../bark-battle/ui/BarkBattleRuntimeShell.tsx | 51 +- .../barkBattleCreationClient.test.ts | 65 ++ .../barkBattleCreationClient.ts | 77 ++ src/services/bark-battle-creation/index.ts | 6 + .../barkBattleRuntimeClient.test.ts | 88 ++ .../barkBattleRuntimeClient.ts | 121 +++ src/services/bark-battle-runtime/index.ts | 7 + 73 files changed, 7933 insertions(+), 107 deletions(-) create mode 100644 .hermes/plans/BARK_BATTLE_PHASE2_PLATFORM_WORK_LOOP_PLAN.md create mode 100644 CONTEXT.md create mode 100644 packages/shared/src/contracts/barkBattle.test.ts create mode 100644 packages/shared/src/contracts/barkBattle.ts create mode 100644 server-rs/crates/api-server/src/bark_battle.rs create mode 100644 server-rs/crates/module-bark-battle/Cargo.toml create mode 100644 server-rs/crates/module-bark-battle/src/domain.rs create mode 100644 server-rs/crates/module-bark-battle/src/lib.rs create mode 100644 server-rs/crates/module-bark-battle/src/scoring.rs create mode 100644 server-rs/crates/shared-contracts/src/bark_battle.rs create mode 100644 server-rs/crates/spacetime-client/src/bark_battle.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_draft_config_row_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_draft_config_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_draft_config_upsert_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_draft_create_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_leaderboard_entry_row_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_leaderboard_entry_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_personal_best_projection_row_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_personal_best_projection_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_procedure_result_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_published_config_row_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_published_config_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_run_finish_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_run_get_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_run_start_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_runtime_config_get_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_runtime_run_row_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_runtime_run_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_score_record_row_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_score_record_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_work_publish_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_work_stats_projection_row_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/bark_battle_work_stats_projection_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/create_bark_battle_draft_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/finish_bark_battle_run_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/get_bark_battle_run_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/get_bark_battle_runtime_config_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/publish_bark_battle_work_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/start_bark_battle_run_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/update_bark_battle_draft_config_procedure.rs create mode 100644 server-rs/crates/spacetime-module/src/bark_battle/mod.rs create mode 100644 server-rs/crates/spacetime-module/src/bark_battle/tables.rs create mode 100644 server-rs/crates/spacetime-module/src/bark_battle/types.rs create mode 100644 src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx create mode 100644 src/components/bark-battle-creation/BarkBattleConfigEditor.tsx create mode 100644 src/components/bark-battle-creation/BarkBattlePreviewCard.tsx create mode 100644 src/services/bark-battle-creation/barkBattleCreationClient.test.ts create mode 100644 src/services/bark-battle-creation/barkBattleCreationClient.ts create mode 100644 src/services/bark-battle-creation/index.ts create mode 100644 src/services/bark-battle-runtime/barkBattleRuntimeClient.test.ts create mode 100644 src/services/bark-battle-runtime/barkBattleRuntimeClient.ts create mode 100644 src/services/bark-battle-runtime/index.ts diff --git a/.hermes/plans/BARK_BATTLE_PHASE2_PLATFORM_WORK_LOOP_PLAN.md b/.hermes/plans/BARK_BATTLE_PHASE2_PLATFORM_WORK_LOOP_PLAN.md new file mode 100644 index 00000000..ded40159 --- /dev/null +++ b/.hermes/plans/BARK_BATTLE_PHASE2_PLATFORM_WORK_LOOP_PLAN.md @@ -0,0 +1,549 @@ +# Bark Battle Phase 2 Platform Work Loop Implementation Plan + +> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task. + +**Goal:** 将 `bark-battle` 从内部试玩 demo 升级为 Genarrative 正式 play type,打通轻创作配置、发布态作品、正式 runtime、run start / finish、后端裁决、个人历史、作品统计和最小排行榜闭环。 + +**Architecture:** 先冻结 shared contracts 与 `module-bark-battle` 纯领域规则,再落 SpacetimeDB 表/reducer、`spacetime-client` facade 和 `api-server` BFF,随后接前端最小纵切,最后补排行榜/个人历史/作品统计投影体验。前端只承接表现、交互和临时 UI 状态,正式业务真相由后端裁决。 + +**Tech Stack:** React + TypeScript + Vite, server-rs + Axum, SpacetimeDB Rust module, shared-contracts, Vitest, Cargo tests, npm scripts. + +--- + +## 0. 已确认决策 + +1. “有效叫声”统一为 **有效声浪触发**:当前采样响度达到有效阈值且满足 `minBarkGapMs` 冷却即触发;不再要求 `minBarkDurationMs` / `maxBarkDurationMs`,也不等待响度回落。 +2. Phase 2 范围是 **Bark Battle 平台作品闭环**,不是单纯玩法表现深化。 +3. 作品形态是 **轻创作配置作品**:标题、描述、主题/背景预设、狗狗皮肤预设、难度预设、排行榜开关。 +4. 难度预设只影响 AI 对手行为;不影响有效阈值、冷却、时长、分数公式或反作弊阈值。 +5. 排行榜按 `workId + difficultyPreset + rulesetVersion` 分榜。 +6. 后端裁决正式单局结果;前端只提交派生指标,`clientResult` 只用于 debug/对账。 +7. 排行榜只收录 `serverResult = player_win` 且未被反作弊拒绝的单局结果,排序以 `finalEnergy` 优先。 +8. 作品统计使用最小后端投影:start、finish、win/draw/loss、flagged、leaderboard、best/avg energy。 +9. 个人历史成绩 = 最近记录列表 + 个人最佳摘要;仅本人可见。 +10. 正式入口闭环覆盖创作入口、作品详情 CTA、广场/作品卡片、我的作品、稳定作品 ID runtime 路由和 `work_play_start`。 +11. 创作编辑形态是单页轻配置表单 + 预览卡片。 +12. 实施顺序固定为:契约与领域规则 → SpacetimeDB 表/reducer 与 api-server BFF → 最小前端纵切 → 投影与列表体验 → 收口验证。 + +--- + +## 1. 必读文档与约束 + +实施前先读: + +- `AGENTS.md` +- `CONTEXT.md` +- `docs/prd/BARK_BATTLE_BDD_2026-05-11.md` +- `docs/technical/BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md` +- `docs/technical/BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md` +- `docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md` +- `docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md` +- `.codex/skills/spacetimedb-cli/SKILL.md` +- `.codex/skills/spacetimedb-rust/SKILL.md` +- `.codex/skills/spacetimedb-concepts/SKILL.md` +- `.codex/skills/spacetimedb-typescript/SKILL.md` + +关键约束: + +- 后端路线固定 `server-rs + Axum + SpacetimeDB`。 +- 领域规则进 `module-bark-battle`,SpacetimeDB 表和事务编排进 `spacetime-module`。 +- HTTP/SSE/BFF 留在 `api-server`。 +- 前后端 DTO 留在 `shared-contracts`。 +- 数据库表结构更改必须同步 `migration.rs` 和生成绑定。 +- 人工命令/文档示例禁止继续使用 `spacetime --root-dir`。 +- 修改中文文件后必须跑 `npm run check:encoding`。 + +--- + +## 2. 阶段一:契约与领域规则 + +### Task 1.1: 新增 Rust shared-contracts 模块 + +**Objective:** 定义 Bark Battle Phase 2 的 Rust DTO 边界。 + +**Files:** +- Create: `server-rs/crates/shared-contracts/src/bark_battle.rs` +- Modify: `server-rs/crates/shared-contracts/src/lib.rs` +- Test: `server-rs/crates/shared-contracts/src/bark_battle.rs` + +**Steps:** +1. 新增枚举:`BarkBattleDifficultyPreset { Easy, Normal, Hard }`、`BarkBattleServerResult { PlayerWin, OpponentWin, Draw }`、`BarkBattleFinishStatus { Accepted, AcceptedWithFlags, Rejected }`。 +2. 新增配置 DTO:`BarkBattleDraftConfig`、`BarkBattlePublishedConfig`、`BarkBattleRuntimeConfig`。 +3. 新增 run DTO:`BarkBattleRunStartRequest/Response`、`BarkBattleRunFinishRequest/Response`。 +4. 新增派生指标 DTO:`BarkBattleDerivedMetrics`,字段包含 `trigger_count`、`max_volume`、`average_volume`、`final_energy`、`combo_max`。 +5. 新增排行榜/历史/统计 DTO:`BarkBattleLeaderboardEntry`、`BarkBattlePersonalHistoryItem`、`BarkBattlePersonalBestSummary`、`BarkBattleWorkStats`。 +6. 在 `lib.rs` 导出 `pub mod bark_battle;`。 + +**Verification:** + +```bash +cargo test -p shared-contracts bark_battle +``` + +Expected: contracts tests pass. + +### Task 1.2: 新增 TypeScript shared contracts mirror + +**Objective:** 让前端获得与 Rust DTO 对齐的类型。 + +**Files:** +- Create: `packages/shared/src/contracts/barkBattle.ts` +- Modify: `packages/shared/src/contracts/index.ts` +- Test: `packages/shared/src/contracts/barkBattle.test.ts` + +**Steps:** +1. 定义 `BarkBattleDifficultyPreset = 'easy' | 'normal' | 'hard'`。 +2. 定义 `BarkBattleServerResult = 'player_win' | 'opponent_win' | 'draw'`。 +3. 定义 draft / published / runtime config 类型。 +4. 定义 start / finish request response 类型。 +5. 定义 leaderboard / personal history / work stats 类型。 +6. 写最小序列化/fixture 测试,确保字段命名采用前端约定 camelCase,并在 API client 层做必要映射。 + +**Verification:** + +```bash +npm test -- --run packages/shared/src/contracts/barkBattle.test.ts +npx tsc -p tsconfig.typecheck-guardrails.json --noEmit --pretty false +``` + +### Task 1.3: 新建 module-bark-battle crate + +**Objective:** 将正式裁决规则放入纯领域 crate。 + +**Files:** +- Create: `server-rs/crates/module-bark-battle/Cargo.toml` +- Create: `server-rs/crates/module-bark-battle/src/lib.rs` +- Create: `server-rs/crates/module-bark-battle/src/domain.rs` +- Create: `server-rs/crates/module-bark-battle/src/scoring.rs` +- Modify: `server-rs/Cargo.toml` + +**Steps:** +1. 在 workspace 中注册 `module-bark-battle`。 +2. 定义 `RulesetVersion`,首版固定如 `bark-battle-ruleset-v1`。 +3. 定义 `BarkBattleRuleset`,包含标准局时长 30s、`min_bark_gap_ms`、合法音量/能量/连击范围、duration tolerance。 +4. 实现 `validate_finish_metrics()`。 +5. 实现 `adjudicate_result()`:以后端 `final_energy` 和 draw threshold 生成 `serverResult`。 +6. 实现 `compute_leaderboard_score()`:只允许胜利局入榜,排序因子为 `finalEnergy`、`triggerCount`、`maxVolume`、duration 接近度、`finishedAt`。 + +**Verification:** + +```bash +cargo test -p module-bark-battle +``` + +### Task 1.4: 领域规则单测覆盖作弊边界 + +**Objective:** 防止前端伪造 finish 直接刷榜。 + +**Files:** +- Modify: `server-rs/crates/module-bark-battle/src/scoring.rs` + +**Test cases:** +- 28s-35s 合法窗口内可接受。 +- 1s / 300s 应 rejected 或 flagged。 +- `triggerCount > durationMs / minBarkGapMs + tolerance` 应 flagged。 +- `finalEnergy` 越界应 rejected。 +- 平/负不生成 leaderboard entry。 +- easy/normal/hard 不改变阈值、冷却、分数公式,只改变 AI preset key。 + +**Verification:** + +```bash +cargo test -p module-bark-battle -- --nocapture +``` + +--- + +## 3. 阶段二:SpacetimeDB 表/reducer 与 api-server BFF + +### Task 2.1: 设计 SpacetimeDB 表目录 + +**Objective:** 新增 Bark Battle 表并与 migration 对齐。 + +**Files:** +- Create: `server-rs/crates/spacetime-module/src/bark_battle/mod.rs` +- Create: `server-rs/crates/spacetime-module/src/bark_battle/types.rs` +- Create: `server-rs/crates/spacetime-module/src/bark_battle/tables.rs` +- Modify: `server-rs/crates/spacetime-module/src/lib.rs` +- Modify: `server-rs/crates/spacetime-module/src/migration.rs` + +**Tables:** +- `bark_battle_draft_config` +- `bark_battle_published_config` +- `bark_battle_runtime_run` +- `bark_battle_score_record` +- `bark_battle_leaderboard_entry` +- `bark_battle_work_stats_projection` +- `bark_battle_personal_best_projection` + +**Pitfalls:** +- 表结构不要 derive `SpacetimeType`。 +- reducer 使用 `&ReducerContext`。 +- 授权身份来自 `ctx.sender()`。 +- 需要公开订阅的表才加 `public`。 + +**Verification:** + +```bash +cargo test -p spacetime-module +``` + +### Task 2.2: 实现草稿/发布 reducer + +**Objective:** 支持轻配置草稿保存和发布态 config 固化。 + +**Reducers:** +- `create_bark_battle_draft` +- `update_bark_battle_draft_config` +- `publish_bark_battle_work` +- `get_bark_battle_runtime_config` 如仓库约定使用 reducer/procedure 查询则按现有 pattern 实现。 + +**Rules:** +- 草稿配置只允许标题、描述、主题/背景预设、狗狗皮肤预设、难度预设、排行榜开关。 +- 发布生成稳定作品 ID / config version。 +- 发布态 config 包含 `rulesetVersion`。 + +**Verification:** + +```bash +cargo test -p spacetime-module bark_battle +``` + +### Task 2.3: 实现 run start / finish reducer + +**Objective:** 打通正式运行态后端事务。 + +**Reducers:** +- `start_bark_battle_run` +- `finish_bark_battle_run` +- `get_bark_battle_run` + +**Rules:** +- start 创建 `run_id` 和一次性 `run_token`。 +- start 记录 work/config/ruleset/difficulty 快照。 +- finish 必须校验 run token、未 finish、work/config/ruleset/difficulty 一致。 +- finish 调用 `module-bark-battle` 裁决结果。 +- accepted 写 score record。 +- `serverResult = player_win` 且排行榜开启且未 rejected 时写 leaderboard entry。 +- accepted / accepted_with_flags 更新 work stats 和 personal best projection。 + +**Verification:** + +```bash +cargo test -p spacetime-module bark_battle_run +``` + +### Task 2.4: 更新 migration 与生成绑定 + +**Objective:** 让 SpacetimeDB 表结构变更可发布。 + +**Files:** +- Modify: `server-rs/crates/spacetime-module/src/migration.rs` +- Generated: `server-rs/crates/spacetime-client/src/module_bindings/*bark*` + +**Commands:** +按仓库现有脚本优先;不要手改 generated bindings。 + +```bash +npm run spacetime:build +npm run spacetime:generate +``` + +若脚本名不同,先查 `package.json` 和 `server-rs` README。 + +### Task 2.5: 实现 spacetime-client facade + +**Objective:** api-server 不直接操作 generated bindings。 + +**Files:** +- Create: `server-rs/crates/spacetime-client/src/bark_battle.rs` +- Modify: `server-rs/crates/spacetime-client/src/lib.rs` + +**Methods:** +- `create_bark_battle_draft` +- `save_bark_battle_draft_config` +- `publish_bark_battle_work` +- `get_bark_battle_runtime_config` +- `start_bark_battle_run` +- `finish_bark_battle_run` +- `list_bark_battle_leaderboard` +- `list_my_bark_battle_history` +- `get_my_bark_battle_best_summary` +- `get_bark_battle_work_stats` + +**Verification:** + +```bash +cargo test -p spacetime-client bark_battle +``` + +### Task 2.6: 实现 api-server BFF 路由 + +**Objective:** 暴露前端需要的 HTTP API。 + +**Files:** +- Create: `server-rs/crates/api-server/src/bark_battle.rs` +- Modify: `server-rs/crates/api-server/src/app.rs` + +**Routes:** +- `POST /api/bark-battle/drafts` +- `PATCH /api/bark-battle/drafts/:draftId` +- `POST /api/bark-battle/drafts/:draftId/publish` +- `GET /api/bark-battle/works/:workId/runtime-config` +- `POST /api/bark-battle/runs/start` +- `POST /api/bark-battle/runs/:runId/finish` +- `GET /api/bark-battle/works/:workId/leaderboard` +- `GET /api/bark-battle/me/history` +- `GET /api/bark-battle/me/best-summary` +- `GET /api/bark-battle/works/:workId/stats` + +**Verification:** + +```bash +cargo test -p api-server bark_battle +npm run api-server +curl -f http://127.0.0.1:/healthz +``` + +--- + +## 4. 阶段三:最小前端纵切 + +### Task 3.1: 新增前端 service client + +**Files:** +- Create: `src/services/bark-battle/barkBattleClient.ts` +- Test: `src/services/bark-battle/barkBattleClient.test.ts` + +**Methods:** 与 BFF routes 一一对应。 + +**Verification:** + +```bash +npm test -- --run src/services/bark-battle/barkBattleClient.test.ts +``` + +### Task 3.2: 接入创作入口与 SelectionStage + +**Files:** +- Modify: `src/config/newWorkEntryConfig.ts` +- Modify: `src/components/platform-entry/platformEntryCreationTypes.ts` +- Modify: `src/components/platform-entry/platformEntryTypes.ts` +- Modify: `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx` + +**Rules:** +- 新增 `bark-battle` play type。 +- 入口打开单页轻配置表单,不走复杂 agent workspace。 +- 移动端入口布局不能溢出。 + +### Task 3.3: 实现单页轻配置表单 + 预览卡片 + +**Files:** +- Create: `src/components/bark-battle-creation/BarkBattleConfigEditor.tsx` +- Create: `src/components/bark-battle-creation/BarkBattlePreviewCard.tsx` +- Test: `src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx` + +**UI fields:** +- 标题必填 +- 简介选填 +- 主题/背景预设 +- 狗狗皮肤预设 +- 难度预设,默认 `normal` +- 排行榜开关,默认开启 + +**UI constraints:** +- 不堆大段玩法说明。 +- 按现有游戏 UI 风格设计。 +- 移动端优先。 + +### Task 3.4: 发布后进入作品详情 + +**Files:** +- Modify: `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx` +- Modify: `src/components/platform-entry/PlatformWorkDetailView.tsx` +- Modify: `src/components/custom-world-home/CustomWorldCreationHub.tsx` +- Modify: `src/components/custom-world-home/creationWorkShelf.ts` + +**Rules:** +- 发布成功刷新 works/gallery/shelf。 +- 跳作品详情。 +- 详情 CTA 可以进入正式 runtime。 + +### Task 3.5: runtime 拉发布态 config 并 start / finish + +**Files:** +- Modify: `src/games/bark-battle/*` +- Modify: `src/games/bark-battle/ui/BarkBattleRuntimeShell.tsx` +- Create/Modify: `src/components/bark-battle-runtime/BarkBattleRuntimeRoute.tsx` 如需要 + +**Rules:** +- runtime 通过稳定 `workId` 拉 `BarkBattleRuntimeConfig`。 +- 开始正式局时调用 start run。 +- 结束时提交 finish 派生指标。 +- 结算展示 `serverResult`、`scoreSummary`、`antiCheatFlags`、leaderboard entry。 +- 麦克风原始音频不上传。 + +**Verification:** + +```bash +npm test -- --run src/games/bark-battle/domain/__tests__/BarkDetector.test.ts src/games/bark-battle/application/__tests__/BarkBattleController.test.ts src/games/bark-battle/ui/__tests__/BarkBattleRuntimeShell.test.tsx +``` + +--- + +## 5. 阶段四:投影与列表体验 + +### Task 4.1: 排行榜 UI + +**Files:** +- Create: `src/components/bark-battle-leaderboard/BarkBattleLeaderboardPanel.tsx` +- Test: `src/components/bark-battle-leaderboard/BarkBattleLeaderboardPanel.test.tsx` +- Modify: `src/components/platform-entry/PlatformWorkDetailView.tsx` + +**Rules:** +- 查询维度 `workId + difficultyPreset + rulesetVersion`。 +- 只展示胜利入榜成绩。 +- 不展示平/负/flagged 历史。 + +### Task 4.2: 个人历史最近记录 + 最佳摘要 UI + +**Files:** +- Create: `src/components/bark-battle-history/BarkBattlePersonalHistoryPanel.tsx` +- Test: `src/components/bark-battle-history/BarkBattlePersonalHistoryPanel.test.tsx` + +**Rules:** +- 默认最近 20 条。 +- 仅本人可见。 +- 可按 workId / difficultyPreset 过滤。 +- flagged 只做轻提示,不展示详细反作弊原因。 + +### Task 4.3: 作品统计展示 + +**Files:** +- Create: `src/components/bark-battle-stats/BarkBattleWorkStatsPanel.tsx` +- Test: `src/components/bark-battle-stats/BarkBattleWorkStatsPanel.test.tsx` + +**Fields:** +- `playStartCount` +- `finishCount` +- `winCount` +- `drawCount` +- `lossCount` +- `flaggedCount` +- `leaderboardEntryCount` +- `bestLeaderboardScore` +- `bestFinalEnergy` +- `averageFinalEnergy` +- `updatedAt` + +### Task 4.4: 广场卡片/我的作品适配 + +**Files:** +- Modify: `src/components/custom-world-home/creationWorkShelf.ts` +- Modify: `src/components/custom-world-home/CustomWorldCreationHub.tsx` +- Modify: `src/components/rpg-entry/rpgEntryWorldPresentation.ts` +- Modify: `src/services/publicWorkCode.ts` 如分享码需要支持 + +**Rules:** +- Bark Battle 作品能展示、打开详情、开始游玩。 +- 不新增独立 Bark Battle 专区。 + +--- + +## 6. 阶段五:收口验证 + +### Task 5.1: 自动测试清单 + +```bash +cargo test -p shared-contracts bark_battle +cargo test -p module-bark-battle +cargo test -p spacetime-module bark_battle +cargo test -p spacetime-client bark_battle +cargo test -p api-server bark_battle +npm test -- --run packages/shared/src/contracts/barkBattle.test.ts +npm test -- --run src/services/bark-battle/barkBattleClient.test.ts +npm test -- --run src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx +npm test -- --run src/games/bark-battle/domain/__tests__/BarkDetector.test.ts src/games/bark-battle/application/__tests__/BarkBattleController.test.ts src/games/bark-battle/ui/__tests__/BarkBattleRuntimeShell.test.tsx +npx tsc -p tsconfig.typecheck-guardrails.json --noEmit --pretty false +npm run check:encoding +git diff --check +``` + +### Task 5.2: 后端 smoke + +1. 按项目脚本启动 SpacetimeDB + api-server,优先使用 `npm run api-server`,不要使用旧命令。 +2. 确认 `/healthz`。 +3. smoke 流程:创建草稿 → 保存配置 → 发布 → 拉 runtime config → start run → finish run → 查询 leaderboard/history/stats。 + +### Task 5.3: 人工验收路径 + +1. 进入创作入口/玩法选择,选择 Bark Battle。 +2. 在单页轻配置表单中填写标题,选择主题、狗狗皮肤、难度,保持排行榜开启。 +3. 保存草稿。 +4. 发布作品。 +5. 发布后自动进入作品详情。 +6. 点击开始游玩进入正式 runtime。 +7. 授权麦克风,完成 30 秒单局。 +8. 结算页显示后端 `serverResult` 和 score summary。 +9. 若胜利,排行榜出现本局成绩。 +10. 我的记录显示最近记录和个人最佳摘要。 +11. 作品详情/作者视角能看到作品统计。 +12. 广场/作品卡片和我的作品入口都能再次进入详情和 runtime。 + +--- + +## 7. 不做范围 + +- 不做实时多人。 +- 不做 ghost replay。 +- 不做 AI 狗叫识别。 +- 不保存原始音频、PCM、waveform 或可还原语音内容。 +- 不做独立 Bark Battle 专区/活动页。 +- 不做挑战分享、好友邀请、多人数房间。 +- 不做复杂编辑器、多步骤向导、规则参数编辑、AI 生成配置。 +- 不做 DAU/留存、按小时统计曲线、好友对比。 + +--- + +## 8. 三人并行建议 + +### 开发者 A:后端契约与领域规则 + +负责 Task 1.1、1.3、1.4。先提交 contracts 与 `module-bark-battle`,为后续后端/前端提供稳定类型和裁决规则。 + +### 开发者 B:SpacetimeDB + api-server + +负责 Task 2.1 到 2.6。必须等开发者 A 的 DTO/领域规则基本稳定后开始,或先基于计划字段开分支实现表结构。 + +### 开发者 C:前端纵切与 UI + +负责 Task 3.x 与 4.x。开始时可先做组件空态和 service client 类型,真正联调等 B 的 BFF ready。 + +--- + +## 9. 推荐提交节奏 + +1. `feat: add bark battle contracts and domain rules` +2. `feat: add bark battle spacetime tables and reducers` +3. `feat: add bark battle api server routes` +4. `feat: add bark battle creation editor` +5. `feat: connect bark battle runtime to server results` +6. `feat: add bark battle leaderboard history stats` +7. `docs: finalize bark battle phase2 verification guide` + +--- + +## 10. 完成定义 + +Phase 2 完成必须同时满足: + +- Bark Battle 可以从正式创作入口创建轻配置作品。 +- 作品可以发布为稳定 workId。 +- 作品详情/广场/我的作品可以发现并进入正式 runtime。 +- runtime 从后端发布态 config 拉配置。 +- start run 写 `work_play_start`。 +- finish 只上传派生指标。 +- 后端裁决 `serverResult` / `scoreSummary` / `leaderboardScore` / `antiCheatFlags`。 +- 胜利局进入按 `workId + difficultyPreset + rulesetVersion` 分榜的排行榜。 +- 个人历史和作品统计可查询。 +- 自动测试、encoding、typecheck、diff check 和人工验收路径通过。 diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 00000000..6b9c8cf0 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,110 @@ +# Genarrative Domain Context + +Genarrative 是一个 AI 原生互动内容与小游戏平台,当前上下文记录团队在玩法、作品、运行态和平台闭环中使用的领域语言。 + +## Language + +### Bark Battle + +**汪汪声浪大作战**: +浏览器 2D 声控对战玩法,玩家通过麦克风响度触发声浪并推动能量条。 +_Avoid_: 狗叫识别游戏、声纹识别玩法 + +**有效声浪触发**: +玩家麦克风采样点的归一化响度在冷却结束后达到或超过有效阈值时产生的一次计分输入。 +_Avoid_: 有效叫声持续时长、狗叫识别结果、等待回落后的叫声 + +**有效阈值**: +用于判定麦克风采样是否产生有效声浪触发的响度门槛。 +_Avoid_: 狗叫识别阈值、语义识别阈值 + +**声浪冷却**: +两次有效声浪触发之间必须满足的最小时间间隔。 +_Avoid_: 叫声持续时长、回落等待时间 + +**能量条**: +表示玩家与对手当前声浪优势的连续对抗刻度。 +_Avoid_: 血条、分数条 + +**单局结果**: +一局 Bark Battle 结束后形成的胜负、平局和派生统计摘要。 +_Avoid_: 原始音频记录、语音内容 + +**Bark Battle 平台作品闭环**: +Bark Battle 从创作配置、发布作品、正式运行态到单局结果记录和作品统计的完整平台流程。 +_Avoid_: 孤立 demo、只做表现深化 + +**轻创作配置作品**: +创作者只配置展示与难度预设字段、但不能直接配置公平性敏感规则的 Bark Battle 作品。 +_Avoid_: 完整规则编辑器、固定官方关卡 + +**难度预设**: +Bark Battle 作品中用于选择 AI 对手行为强度的发布态配置值。 +_Avoid_: 阈值配置、分数公式配置 + +**排行榜分榜**: +排行榜按作品、难度预设和规则集版本拆分后的独立排名空间,只收录后端裁决为玩家胜利的单局结果。 +_Avoid_: 全难度混排、跨规则版本混排、失败刷分榜 + +**后端裁决结果**: +后端根据 start run 记录和 finish 派生指标校验后生成的正式单局结果。 +_Avoid_: 前端最终分数、客户端胜负裁决 + +**派生指标**: +前端从本地 runtime 汇总出的不可还原原始音频的单局统计值。 +_Avoid_: 原始音频、可还原语音内容 + +**作品统计投影**: +按作品聚合的 Bark Battle 游玩开始、完成结果、反作弊标记和最佳/平均表现摘要。 +_Avoid_: 只从排行榜反推、原始音频分析、留存分析 + +**个人历史成绩**: +玩家本人可查看的 Bark Battle 最近完成记录和个人最佳摘要。 +_Avoid_: 公开失败记录、完整无限历史、好友对比 + +**正式作品入口闭环**: +Bark Battle 作品从创作入口、作品详情、广场/作品卡片、我的作品到正式 runtime 路由的可发现、可进入流程。 +_Avoid_: 内部试玩入口、独立活动专区 + +**轻配置编辑流程**: +Bark Battle 创作者用单页轻配置表单和预览卡片完成草稿保存与发布的创作流程。 +_Avoid_: 多步骤向导、完整规则编辑器、拖拽编辑器 + +**Phase 2 实施顺序**: +Bark Battle 平台作品闭环按契约与领域规则、后端存储/API、最小前端纵切、投影体验、收口验证的顺序推进。 +_Avoid_: mock 先行堆积、前后端各自发散、先做排行榜 UI + +## Relationships + +- 一个 **汪汪声浪大作战** 单局包含多个 **有效声浪触发**。 +- 每个 **有效声浪触发** 必须达到 **有效阈值** 并满足 **声浪冷却**。 +- **有效声浪触发** 推动 **能量条**。 +- **能量条** 在倒计时结束时产生一个 **单局结果**。 +- **单局结果** 可以被后端记录为派生摘要,但不包含原始麦克风音频。 +- **Bark Battle 平台作品闭环** 包含发布态作品配置、**正式作品入口闭环**、run start / finish、个人历史成绩、**作品统计投影** 和最小排行榜。 +- Phase 2 的 Bark Battle 作品是 **轻创作配置作品**,通过 **轻配置编辑流程** 创建;配置范围限制为标题、描述、主题/背景预设、狗狗皮肤预设和排行榜开关,其中 **难度预设** 只影响 AI 对手行为。 +- **排行榜分榜** 由 `workId + difficultyPreset + rulesetVersion` 唯一确定,只收录 `serverResult = player_win` 的单局结果。 +- **单局结果** 的正式胜负、分数与排行榜成绩来自 **后端裁决结果**;前端只提交 **派生指标**,客户端结果仅用于 debug/对账。 +- **作品统计投影** 计入成功 start run 的 playStartCount、后端接受 finish 的 finishCount、胜/平/负、flagged、leaderboard 以及最佳/平均能量表现。 +- **个人历史成绩** 由最近记录列表和个人最佳摘要组成,只允许本人查看;排行榜只公开入榜胜利成绩。 +- **正式作品入口闭环** 必须覆盖创作入口、作品详情 CTA、广场/作品卡片、我的作品/个人作品架、稳定作品 ID runtime 路由和 `work_play_start` 埋点。 +- **Phase 2 实施顺序** 固定为:契约与领域规则 → SpacetimeDB 表/reducer 与 api-server BFF → 最小前端纵切 → 投影与列表体验 → 收口验证。 + +## Example dialogue + +> **Dev:** “第二阶段排行榜要按玩家狗叫持续时间排序吗?” +> **Domain expert:** “不按持续时间;Bark Battle 的计分输入是有效声浪触发,排行榜只能基于触发次数、峰值、能量条结果等派生摘要。” + +## Flagged ambiguities + +- “有效叫声”曾同时指代持续时长合规的声音片段和瞬时响度触发;已解析为 **有效声浪触发**,不再要求 `minBarkDurationMs` / `maxBarkDurationMs`,也不等待响度回落。 +- “第二阶段”曾可能指玩法表现深化或平台接入;已解析为 **Bark Battle 平台作品闭环**,优先补正式 play type、作品配置、发布、正式 runtime、结果持久化、历史成绩、作品统计和最小排行榜。 +- “创作者可配置作品”曾可能指完整规则编辑器;已解析为 **轻创作配置作品**,Phase 2 不允许创作者直接配置单局时长、有效阈值、声浪冷却、AI 细粒度参数、分数公式或反作弊阈值。 +- “难度预设”曾可能影响阈值、冷却或计分;已解析为只影响 AI 对手行为,排行榜按 `workId + difficultyPreset + rulesetVersion` 分榜。 +- “单局结果”曾可能由前端直接决定;已解析为必须由 **后端裁决结果** 决定,前端只提交触发次数、音量、能量、连击、时长等 **派生指标**。 +- “排行榜成绩”曾可能收录胜/平/负或按触发次数排序;已解析为只收录玩家胜利局,并以 `finalEnergy` 优先、`triggerCount` / `maxVolume` / 标准局时长接近度 / `finishedAt` 作为后续排序因子。 +- “作品统计”曾可能只从排行榜反推;已解析为独立 **作品统计投影**,失败、平局和 flagged finish 都可进入统计,但 rejected finish 不进入完成统计。 +- “个人历史成绩”曾可能指完整无限历史或公开记录;已解析为仅本人可见的最近记录列表 + 个人最佳摘要,不公开失败、平局或 flagged 历史。 +- “入口闭环”曾可能只指内部 demo 或单个详情 CTA;已解析为 **正式作品入口闭环**,不新增独立专区或活动页。 +- “创作编辑”曾可能指多步骤向导或完整编辑器;已解析为 **轻配置编辑流程**,使用单页表单 + 预览卡片完成保存草稿、发布和发布后跳转作品详情。 +- “实施顺序”曾可能按 UI 或功能并行发散;已解析为契约/领域规则先行,再做后端存储/API,随后打通最小前端纵切,最后补投影体验与收口验证。 diff --git a/docs/prd/BARK_BATTLE_BDD_2026-05-11.md b/docs/prd/BARK_BATTLE_BDD_2026-05-11.md index bc3fd29c..d671a4ba 100644 --- a/docs/prd/BARK_BATTLE_BDD_2026-05-11.md +++ b/docs/prd/BARK_BATTLE_BDD_2026-05-11.md @@ -4,7 +4,7 @@ - 需求来源:用户提供的视频 `C:\Users\DSK\Videos\一款双方比狗叫的游戏 - 1.一款双方比狗叫的游戏(Av116504192360177,P1).mp4`,并已在 `.hermes/plans/2026-05-11_144229-bark-battle-2d-game-bdd-ddd-tdd-plan.md` 中完成抽帧分析和玩法方案整理。 - 玩法定位:浏览器 2D 声控狗叫对战小游戏,暂定中文名 `汪汪声浪大作战`,英文代号与 play type ID 建议为 `bark-battle`。 -- 核心玩法:双方狗狗在 30 秒限时内通过麦克风输入“狗叫声”进行声浪拔河;系统依据声音强度、有效叫声次数和叫声节奏计算推动力,实时推动顶部红蓝能量条;倒计时结束后按能量条位置判定胜负或平局。 +- 核心玩法:双方狗狗在 30 秒限时内通过麦克风输入“狗叫声”进行声浪拔河;系统依据声音强度、有效声浪触发次数和声浪节奏计算推动力,实时推动顶部红蓝能量条;倒计时结束后按能量条位置判定胜负或平局。 - 文档目的:为产品、测试、前端、后端在编码前统一可验证验收口径;本文只定义 PRD/BDD 级行为与测试映射,不实现工程代码。 ## 角色与目标 @@ -20,14 +20,14 @@ ### 用户目标 - 玩家可以在开局前完成麦克风授权和环境噪音校准。 -- 玩家发出有效狗叫时,能看到叫声计数、狗狗动画、拟声词/冲击波以及能量条变化。 -- 低于阈值的背景噪音不会被误计为有效叫声。 +- 玩家产生有效声浪触发时,能看到声浪计数、狗狗动画、拟声词/冲击波以及能量条变化。 +- 低于阈值的背景噪音不会被误计为有效声浪触发。 - 单局在 30 秒后给出明确胜负、平局和关键数据。 - 移动端和不支持麦克风的环境不会进入不可操作状态。 ### 非目标 -- MVP 不要求识别“是否真实狗叫”,不引入机器学习声纹/物种分类;有效输入以音量阈值、峰值间隔、持续时间和校准结果为准。 +- MVP 不要求识别“是否真实狗叫”,不引入机器学习声纹/物种分类;有效输入以音量阈值、峰值冷却间隔和校准结果为准。 - MVP 不要求实时联机对战;可先按“玩家 vs AI 对手”完成单机浏览器 runtime。 - MVP 不要求成绩持久化、作品发布、作品架、广场和排行榜;若后续接入 Genarrative 作品闭环,需要另补玩法类型集成 PRD/技术文档。 - MVP 不要求在 UI 中长期展示大段规则说明;游戏界面应保持倒计时、能量条、狗狗、麦克风状态和结算信息为主。 @@ -38,10 +38,10 @@ - 单局时长:默认 30 秒,从正式进入 `playing` 阶段开始计时。 - 能量条:使用 `-100` 到 `100` 的连续值表示,负数偏对手侧,正数偏玩家侧,`0` 为中线。 - 平局阈值:倒计时结束时,若能量条绝对值小于或等于 `drawThreshold`,判定平局;具体数值由实现配置,但测试需可注入固定阈值。 -- 有效叫声:一次有效叫声至少满足:音量超过校准后的有效阈值、与上一次有效峰值间隔不小于 `minBarkGapMs`、持续时长在 `minBarkDurationMs` 到 `maxBarkDurationMs` 之间。 -- 背景噪音:校准阶段采集到的环境声用于计算动态阈值;低于阈值的输入不得增加叫声次数,也不得让能量条出现可见推进。 -- 推动力:玩家推动力由音量分数、有效叫声频率和连击加成组成;能量条按玩家推动力与对手推动力差值移动,并被限制在 `-100` 到 `100`。 -- UI 反馈:有效叫声应触发可观察反馈,包括玩家侧狗狗张嘴/吠叫动画、拟声词或冲击波;反馈不应遮挡倒计时和顶部能量条。 +- 有效声浪触发:一次有效声浪触发满足:当前麦克风采样点的归一化响度达到或超过校准后的有效阈值,且与上一次有效声浪触发间隔不小于 `minBarkGapMs`;不再要求持续高响度时长达标,也不等待响度回落。 +- 背景噪音:校准阶段采集到的环境声用于计算动态阈值;低于阈值的输入不得增加声浪触发次数,也不得让能量条出现可见推进。 +- 推动力:玩家推动力由音量分数、有效声浪触发频率和连击加成组成;能量条按玩家推动力与对手推动力差值移动,并被限制在 `-100` 到 `100`。 +- UI 反馈:有效声浪触发应触发可观察反馈,包括玩家侧狗狗张嘴/吠叫动画、拟声词或冲击波;反馈不应遮挡倒计时和顶部能量条。 ## 中文 Gherkin 场景 @@ -66,7 +66,7 @@ 场景: 校准完成后进入开局倒计时 假如玩家已允许麦克风权限 而且系统已采集足够的环境噪音样本 - 当校准计算出有效叫声阈值 + 当校准计算出有效声浪阈值 那么系统应进入开局倒计时阶段 而且倒计时结束后应进入 30 秒对战阶段 而且初始能量条应位于中线 @@ -84,19 +84,19 @@ 功能: 环境噪音校准 为了减少背景噪音误触发 作为浏览器玩家 - 我希望游戏在开局前根据当前环境设置有效叫声阈值 + 我希望游戏在开局前根据当前环境设置有效声浪阈值 场景: 安静环境生成低但非零的有效阈值 假如校准阶段采集到的环境噪音 RMS 稳定低于默认噪音基线 当系统完成校准 - 那么有效叫声阈值应高于环境噪音平均值 + 那么有效声浪阈值应高于环境噪音平均值 而且阈值不应低于系统配置的最小阈值 场景: 嘈杂环境生成更高的有效阈值 假如校准阶段采集到的环境噪音 RMS 高于默认噪音基线 当系统完成校准 - 那么有效叫声阈值应随环境噪音上调 - 而且低于该阈值的后续输入不应计为有效叫声 + 那么有效声浪阈值应随环境噪音上调 + 而且低于该阈值的后续输入不应计为有效声浪触发 场景: 校准期间无法获得有效音频样本 假如麦克风授权成功但音频样本持续为空或不可读 @@ -106,36 +106,36 @@ 而且不应直接开始对战 ``` -### 功能: 有效叫声计数 +### 功能: 有效声浪触发计数 ```gherkin -功能: 有效叫声计数 - 为了把玩家的狗叫行为转换为可计分输入 +功能: 有效声浪触发计数 + 为了把玩家的声控行为转换为可计分输入 作为玩家 - 我希望每次符合规则的短促叫声只被计数一次 + 我希望每次超过阈值且满足冷却的声浪触发只被计数一次 背景: 假如游戏处于 30 秒 playing 阶段 而且系统已完成环境噪音校准 - 场景: 单次超过阈值且间隔足够的叫声计数加一 - 假如玩家当前叫声次数为 0 - 而且上一次有效叫声时间早于 minBarkGapMs - 当麦克风输入出现一次超过有效阈值且持续时长合规的峰值 - 那么玩家叫声次数应变为 1 + 场景: 单次超过阈值且间隔足够的声浪触发计数加一 + 假如玩家当前声浪触发次数为 0 + 而且上一次有效声浪触发时间早于 minBarkGapMs + 当某个麦克风采样点达到或超过有效阈值且满足声浪冷却 + 那么玩家声浪触发次数应变为 1 而且玩家侧应出现一次吠叫动画反馈 而且画面应出现一次拟声词或冲击波反馈 - 场景: 持续噪音不会被无限计数 - 假如玩家当前叫声次数为 1 - 当麦克风输入持续超过阈值但没有新的峰值间隔 - 那么玩家叫声次数不应在每个 tick 中持续增加 - 而且系统最多只应记录当前连续声音段内的一次有效叫声 + 场景: 持续高响度输入只按冷却节奏计数 + 假如玩家当前声浪触发次数为 1 + 当麦克风输入持续超过阈值但仍处于声浪冷却内 + 那么玩家声浪触发次数不应在每个 tick 中持续增加 + 而且系统只应在冷却结束后的采样点再次达阈值时记录下一次有效声浪触发 场景: 间隔过短的连续峰值不重复计数 - 假如玩家刚刚产生一次有效叫声 - 当麦克风输入在 minBarkGapMs 内再次出现峰值 - 那么玩家叫声次数不应增加 + 假如玩家刚刚产生一次有效声浪触发 + 当麦克风输入在 minBarkGapMs 内再次达到有效阈值 + 那么玩家声浪触发次数不应增加 而且连击或推动力不应因该峰值重复加成 ``` @@ -145,23 +145,23 @@ 功能: 声浪推动能量条 为了复刻双方比狗叫的核心体验 作为玩家 - 我希望更响、更连续的有效叫声能把顶部能量条推向自己一侧 + 我希望更响、更高频的有效声浪触发能把顶部能量条推向自己一侧 背景: 假如游戏处于 30 秒 playing 阶段 而且能量条当前位于中线 场景: 玩家推动力高于对手时能量条向玩家侧移动 - 假如玩家在短时间窗口内产生多次有效叫声 + 假如玩家在短时间窗口内产生多次有效声浪触发 而且玩家推动力高于对手推动力 当系统推进一个 simulation tick 那么能量条数值应向玩家侧增加 而且顶部红蓝能量条的玩家侧占比应变大 - 场景: 连续大声叫声触发更强反馈 - 假如玩家连续产生多次高于强叫声阈值的有效叫声 + 场景: 连续强声浪触发触发更强反馈 + 假如玩家连续产生多次高于强声浪阈值的有效声浪触发 当系统计算玩家连击加成 - 那么玩家侧推动力应高于单次普通叫声推动力 + 那么玩家侧推动力应高于单次普通声浪触发推动力 而且玩家侧声浪或冲击波反馈应比普通叫声更明显 但是反馈不应遮挡倒计时和能量条 @@ -185,31 +185,30 @@ 功能: 背景噪音过滤 为了避免环境声替玩家自动得分 作为玩家 - 我希望低于阈值或不合规的声音不会被当作有效狗叫 + 我希望低于阈值或处于冷却内的声音不会被当作有效声浪触发 背景: 假如游戏处于 30 秒 playing 阶段 而且系统已完成环境噪音校准 场景: 低于阈值的背景噪音不计数 - 当麦克风只接收到低于有效叫声阈值的背景噪音 - 那么玩家叫声次数不应增加 + 当麦克风只接收到低于有效声浪阈值的背景噪音 + 那么玩家声浪触发次数不应增加 而且玩家侧不应播放吠叫动画 而且能量条不应因为该背景噪音出现可见推进 - 场景: 过短脉冲不计为有效叫声 - 假如麦克风输入峰值超过有效阈值 - 但是持续时长短于 minBarkDurationMs - 当系统完成该声音段判定 - 那么玩家叫声次数不应增加 + 场景: 冷却内重复达阈值不计数 + 假如玩家刚刚产生一次有效声浪触发 + 当麦克风输入在 minBarkGapMs 内再次达到有效声浪阈值 + 那么玩家声浪触发次数不应增加 而且不应触发连击加成 - 场景: 过长持续声被削弱为单段输入 - 假如麦克风输入持续超过有效阈值 - 但是持续时长长于 maxBarkDurationMs - 当系统完成该声音段判定 - 那么系统不应按多个叫声重复计数 - 而且该声音段的推动力应按持续噪音削弱规则处理 + 场景: 持续高响度输入只按冷却节奏产生触发 + 假如麦克风输入持续达到或超过有效声浪阈值 + 当声浪冷却尚未结束 + 那么系统不应在每个 tick 中重复计数 + 当声浪冷却结束且当前采样仍达到有效声浪阈值 + 那么系统可以记录下一次有效声浪触发 ``` ### 功能: 倒计时与胜负结算 @@ -227,20 +226,20 @@ 当系统时间从 30 秒推进到 0 秒 那么界面应显示倒计时归零 而且系统应进入 finished 结算阶段 - 而且归零后的麦克风输入不应再改变本局能量条和叫声次数 + 而且归零后的麦克风输入不应再改变本局能量条和声浪触发次数 场景: 玩家侧占优时判定玩家胜利 假如倒计时归零时能量条数值大于 drawThreshold 当系统进入结算阶段 那么系统应判定玩家胜利 - 而且结算面板应展示玩家叫声次数、最大音量和声浪评分 + 而且结算面板应展示玩家声浪触发次数、最大音量和声浪评分 而且应提供再来一局入口 场景: 对手侧占优时判定玩家失败 假如倒计时归零时能量条数值小于 -drawThreshold 当系统进入结算阶段 那么系统应判定对手胜利 - 而且结算面板应展示玩家叫声次数、最大音量和声浪评分 + 而且结算面板应展示玩家声浪触发次数、最大音量和声浪评分 而且应提供再来一局入口 场景: 能量条接近平衡时判定平局 @@ -265,7 +264,7 @@ 当玩家选择再来一局 那么系统应重置剩余时间为 30 秒 而且能量条应回到中线 - 而且玩家叫声次数、最大音量、连击和胜负结果应清零 + 而且玩家声浪触发次数、最大音量、连击和胜负结果应清零 而且系统应重新进入校准或开局倒计时流程 场景: 结算后返回玩法入口 @@ -353,7 +352,7 @@ 假如玩家在 playing 阶段刷新页面 当页面重新加载 bark-battle 那么系统应重新进入权限检查或授权准备状态 - 而且不应沿用刷新前的剩余时间、能量条和叫声次数作为新局结果 + 而且不应沿用刷新前的剩余时间、能量条和声浪触发次数作为新局结果 ``` ## 测试映射 @@ -367,15 +366,15 @@ | 嘈杂环境生成更高的有效阈值 | unit | `src/games/bark-battle/domain/BarkNoiseCalibration.test.ts` | planned | | 校准期间无法获得有效音频样本 | application/component | `src/games/bark-battle/application/BarkBattleController.test.ts`, `src/games/bark-battle/ui/BarkBattlePermissionPanel.test.tsx` | planned | | 单次超过阈值且间隔足够的叫声计数加一 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned | -| 持续噪音不会被无限计数 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned | +| 持续高响度输入只按冷却节奏计数 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned | | 间隔过短的连续峰值不重复计数 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned | | 玩家推动力高于对手时能量条向玩家侧移动 | unit | `src/games/bark-battle/domain/EnergyTugOfWar.test.ts` | planned | -| 连续大声叫声触发更强反馈 | unit/integration/component | `src/games/bark-battle/domain/BarkBattleScoring.test.ts`, `src/games/bark-battle/ui/BarkBattleHud.test.tsx` | planned | +| 连续强声浪触发触发更强反馈 | unit/integration/component | `src/games/bark-battle/domain/BarkBattleScoring.test.ts`, `src/games/bark-battle/ui/BarkBattleHud.test.tsx` | planned | | 能量条到达边界后不会越界 | unit | `src/games/bark-battle/domain/EnergyTugOfWar.test.ts` | planned | | 对手推动力高于玩家时能量条向对手侧移动 | unit | `src/games/bark-battle/domain/EnergyTugOfWar.test.ts` | planned | | 低于阈值的背景噪音不计数 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned | -| 过短脉冲不计为有效叫声 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned | -| 过长持续声被削弱为单段输入 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned | +| 冷却内重复达阈值不计数 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned | +| 持续高响度输入只按冷却节奏产生触发 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned | | 倒计时每秒递减并在归零时停止对战输入 | unit/application | `src/games/bark-battle/domain/BarkBattleSession.test.ts`, `src/games/bark-battle/application/BarkBattleController.test.ts` | planned | | 玩家侧占优时判定玩家胜利 | unit/component | `src/games/bark-battle/domain/BarkBattleSession.test.ts`, `src/games/bark-battle/ui/BarkBattleResultPanel.test.tsx` | planned | | 对手侧占优时判定玩家失败 | unit/component | `src/games/bark-battle/domain/BarkBattleSession.test.ts`, `src/games/bark-battle/ui/BarkBattleResultPanel.test.tsx` | planned | @@ -394,8 +393,8 @@ ## 验收清单 - [ ] 权限允许、拒绝、非安全上下文、API 不支持、麦克风未找到/不可读、AudioContext 被拦截、校准超时或样本不可读均有明确状态,且不会误进入 playing。 -- [ ] 校准阶段会影响有效叫声阈值,低噪音不会增加叫声计数。 -- [ ] 有效叫声计数具备阈值、峰值间隔、持续时长约束。 +- [ ] 校准阶段会影响有效声浪阈值,低噪音不会增加叫声计数。 +- [ ] 有效声浪触发计数具备阈值与声浪冷却约束。 - [ ] 能量条根据双方推动力差值双向移动,并限制在 `-100` 到 `100`。 - [ ] 30 秒归零后停止本局输入影响,并按玩家胜利、对手胜利、平局三类结果结算。 - [ ] 移动端核心元素可见,非关键设置收起,不在主画面堆叠长规则说明。 @@ -405,8 +404,8 @@ ## 开放问题 1. MVP 是否确认只做“玩家 vs AI”,还是第一版需要双人同屏或联机对战? -2. `drawThreshold`、`minBarkGapMs`、`minBarkDurationMs`、`maxBarkDurationMs` 的首版默认值由产品/调参阶段确认,还是先采用开发可配置默认值? +2. `drawThreshold`、`minBarkGapMs`、有效声浪阈值 的首版默认值由产品/调参阶段确认,还是先采用开发可配置默认值? 3. 是否允许无麦克风设备提供键盘/点击备用输入?若允许,需要另补非声控模式场景;若不允许,当前降级只提供返回入口。 -4. 是否需要在结算中记录或上报成绩、最高音量、叫声次数和声浪评分?若需要,需补埋点/后端持久化场景。 +4. 是否需要在结算中记录或上报成绩、最高音量、声浪触发次数和声浪评分?若需要,需补埋点/后端持久化场景。 5. bark-battle 是否作为 Genarrative 正式 play type 接入创作入口、作品发布和广场,还是先作为独立 runtime 原型验证? 6. 狗狗、背景、拟声词和冲击波素材来源是临时占位、AI 生成,还是复用项目现有素材管线? diff --git a/docs/technical/BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md b/docs/technical/BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md index 69ed4771..77202297 100644 --- a/docs/technical/BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md +++ b/docs/technical/BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md @@ -4,7 +4,7 @@ ### 1.1 背景 -`bark-battle` / “汪汪声浪大作战”是一个浏览器 2D 声控狗叫对战玩法。玩家通过麦克风发出狗叫声,浏览器 runtime 根据音量峰值、有效叫声次数与节奏推动顶部红蓝能量条;每局默认 30 秒;结束后按能量条偏向判定胜负或平局。 +`bark-battle` / “汪汪声浪大作战”是一个浏览器 2D 声控狗叫对战玩法。玩家通过麦克风发出狗叫声,浏览器 runtime 根据音量峰值、有效声浪触发次数与节奏推动顶部红蓝能量条;每局默认 30 秒;结束后按能量条偏向判定胜负或平局。 现有前端方案 `docs/technical/BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md` 已覆盖 Phaser / TypeScript / Vite / Web Audio / DOM HUD 的 runtime 落地方式,并明确不覆盖后端表结构、成绩持久化、作品发布、广场接入与实时多人协议。因此需要单独补充后端 DDD 技术方案,避免前端 runtime 在接入平台作品、正式游玩埋点、成绩、排行榜和发布闭环时承接不属于表现层的业务真相。 @@ -44,26 +44,80 @@ MVP 明确不做: ## 2. 玩法接入级别建议 -### 2.1 推荐首版闭环 +### 2.1 第二阶段范围:平台作品闭环 -建议先支持“本地 runtime + 可发布配置化作品 + 单局结果记录 / 可选排行榜”的闭环: +第二阶段已明确为“Bark Battle 平台作品闭环”,不是单纯玩法表现深化。目标是让 bark-battle 成为 Genarrative 的正式 play type,并完成从轻创作配置、发布、正式 runtime、run start / finish、单局结果持久化、个人历史成绩、作品统计到最小排行榜的闭环。 -1. 创作者创建 bark-battle 草稿,配置标题、描述、狗狗主题、背景、难度、单局时长、音量阈值、AI 对手参数和排行榜开关。 +Phase 2 的作品配置边界是“轻创作配置作品”:创作者可以配置标题、描述、主题/背景预设、狗狗皮肤预设、难度预设和排行榜开关;不得直接配置单局时长、有效声浪阈值、`minBarkGapMs`、AI 对手细粒度参数、分数公式或反作弊阈值。难度预设只影响 AI 对手行为强度,不影响有效阈值、声浪冷却、单局时长或分数公式;排行榜必须按 `workId + difficultyPreset + rulesetVersion` 分榜,避免不同难度和不同规则版本混排。 + +建议先支持“本地 runtime + 可发布配置化作品 + 单局结果记录 + 个人历史成绩 / 作品统计 / 最小排行榜”的闭环: + +1. 创作者从玩法选择进入 bark-battle 后创建草稿,通过单页轻配置表单 + 预览卡片配置标题、描述、主题/背景预设、狗狗皮肤预设、难度预设和排行榜开关。 2. 发布为稳定作品 ID,`playTypeId = "bark-battle"`。 -3. 玩家从作品页或广场进入 runtime,前端获取发布态 runtime config。 +3. 玩家可从作品详情页 CTA、广场/作品卡片、我的作品/个人作品架进入正式 runtime,前端使用稳定作品 ID 获取发布态 runtime config。 4. 玩家授权麦克风后在本地完成 30 秒声控对战。 -5. 前端提交单局 finish 请求,只上传派生指标,例如峰值、有效叫声次数、节奏命中、最终能量、客户端结果摘要等。 -6. 后端校验 work、config version、run token、时长、分数范围和权限后,生成服务端认可的 run result / score summary。 -7. 若作品开启排行榜,则写入可投影的 leaderboard 记录。 -8. 正式作品级游玩埋点统一写 `work_play_start`,其中 `scope_kind=work`,`scope_id=稳定作品 ID`,metadata 包含 `playType`、`workId`、`sourceRoute`、`userId`。 +5. 前端提交单局 finish 请求,只上传派生指标,例如峰值、有效声浪触发次数、节奏命中、最终能量、客户端结果摘要等。 +6. 后端校验 work、config version、ruleset version、difficulty preset、run token、时长、派生指标范围和权限后,生成服务端裁决的 run result / score summary。 +7. 写入个人历史成绩与最小作品统计投影。 +8. 若作品开启排行榜且后端裁决 `serverResult = player_win`,则写入可投影的 leaderboard 记录;排行榜首版只做最小排序与展示,不引入赛季、段位或复杂反作弊,并按 `workId + difficultyPreset + rulesetVersion` 分榜。 +9. 正式作品级游玩埋点统一写 `work_play_start`,其中 `scope_kind=work`,`scope_id=稳定作品 ID`,metadata 包含 `playType`、`workId`、`sourceRoute`、`userId`。 + + +### 2.2.1 难度预设与排行榜分榜 + +Phase 2 只允许三个难度预设:`easy`、`normal`、`hard`。难度预设只能影响 AI 对手推动力曲线和 AI 声浪节奏;不得影响单局时长、有效声浪阈值、`minBarkGapMs`、分数公式或反作弊阈值。排行榜记录和查询必须带上 `difficultyPreset` 与 `rulesetVersion`,以 `workId + difficultyPreset + rulesetVersion` 作为分榜维度。 + +### 2.2.2 单局结果后端裁决 + +Phase 2 不信任前端提交的最终胜负和正式分数。前端 `finish` 只提交不可还原原始音频的派生指标:`runId`、`workId`、`configVersion`、`rulesetVersion`、`difficultyPreset`、`clientStartedAt`、`clientFinishedAt`、`durationMs`、`triggerCount`、`maxVolume`、`averageVolume`、`finalEnergy`、`comboMax`、`clientResult`,以及可选的 `sampleDigest`。其中 `clientResult` 只用于 debug/对账,不进入正式结果或排行榜。 + +后端必须校验 run 由 start 创建且未 finish、run token 匹配、work/config/ruleset/difficulty 与 start 时一致、duration 处于合理窗口、triggerCount 不超过 `durationMs / minBarkGapMs + tolerance`、音量/能量/连击字段在合法范围内。后端生成 `serverResult`、`scoreSummary`、`leaderboardScore` 和 `antiCheatFlags`,排行榜只使用后端裁决后的胜利局成绩。 + +### 2.2.3 排行榜排序口径 + +Phase 2 排行榜只收录 `serverResult = player_win` 且未被反作弊规则拒绝的单局结果;平局和失败仍进入个人历史成绩与作品统计,但不进入排行榜。`leaderboardScore` 由后端规则集生成,排序优先级为:`finalEnergy` 降序、`triggerCount` 降序、`maxVolume` 降序、`durationMs` 越接近标准局时长越优、`finishedAt` 越早越优。 + +### 2.2.4 作品统计投影口径 + +Phase 2 的作品统计是最小后端投影,不从排行榜反推。`playStartCount` 在 start run 成功时计入一次,并对齐 `work_play_start` 埋点;`finishCount` 在 finish 被后端接受时计入一次,包含胜利、平局和失败。`accepted_with_flags` 可以计入 `finishCount`,但必须同时计入 `flaggedCount`;未 start 成功、run token 不合法、重复 finish、被后端 rejected 的结果不计入 `finishCount`。 + +作品统计字段首版包含:`playStartCount`、`finishCount`、`winCount`、`drawCount`、`lossCount`、`flaggedCount`、`leaderboardEntryCount`、`bestLeaderboardScore`、`bestFinalEnergy`、`averageFinalEnergy`、`updatedAt`。Phase 2 不做 DAU/留存、按小时曲线、原始音频分析或每玩家每天聚合统计。 + +### 2.2.5 个人历史成绩口径 + +Phase 2 的个人历史成绩由“最近记录列表 + 个人最佳摘要”组成,并且只允许本人查询。后端可以保存每次被接受的 finish 记录,但首版查询接口只暴露默认最近 20 条记录,可按 `workId` 和 `difficultyPreset` 过滤;最近记录包含胜利、平局、失败和是否 flagged,但不展示详细反作弊原因。 + +个人最佳摘要按 `userId + workId + difficultyPreset + rulesetVersion` 聚合,字段包含 `bestLeaderboardScore`、`bestFinalEnergy`、`bestTriggerCount`、`bestMaxVolume`、`winCount`、`finishCount`、`lastPlayedAt`。失败、平局和 flagged 历史不对其他玩家公开;排行榜只展示公开入榜的胜利成绩。Phase 2 不做无限滚动完整历史、每日/每周曲线、好友对比或普通玩家可见的详细反作弊说明。 + +### 2.2.6 正式作品入口闭环 + +Phase 2 必须接入 Bark Battle 正式作品入口闭环,但不新增独立专区、活动页、挑战分享页、好友邀请或多人房间入口。入口范围包括:创作入口/玩法选择中出现 `bark-battle`,进入单页轻配置表单 + 预览卡片;作品详情页 CTA 点击“开始游玩”进入正式 runtime;广场/作品卡片可以展示、打开详情并开始游玩;我的作品/个人作品架能看到作者发布的 Bark Battle 作品;runtime 路由使用稳定作品 ID 并从后端发布态 config 拉取配置。 + +正式 run start 成功后必须写 `work_play_start`,其中 `scope_kind=work`、`scope_id=稳定作品 ID`,metadata 至少包含 `playType=bark-battle`、`workId`、`sourceRoute`、`userId`。内部试玩入口可以作为开发调试保留,但不得作为 Phase 2 正式入口。 + +### 2.2.7 轻配置编辑流程 + +Phase 2 的创作编辑形态是“单页轻配置表单 + 预览卡片”,不是多步骤向导、拖拽编辑器或完整规则编辑器。表单字段包含:标题(必填)、简介(选填)、主题/背景预设(必填枚举)、狗狗皮肤预设(必填枚举)、难度预设(必填,默认 `normal`)、排行榜开关(默认开启)。 + +交互流程:创作者从玩法选择进入后生成草稿;在同一页编辑轻配置并查看预览卡片;支持保存草稿和发布;发布成功后跳转作品详情;可从我的作品再次编辑草稿或基于已发布作品创建新版本。Phase 2 不做 AI 生成配置、多步骤 wizard、规则参数编辑、复杂封面编辑、runtime 内嵌预览或大段玩法说明文案。 ### 2.2 后续增强路径 -后续再考虑多人实时: +第二阶段之后再考虑: -- Phase 2:排行榜、挑战分享、个人历史成绩、作品统计面板。 +- Phase 2.1:挑战分享、作品统计面板细化、排行榜体验优化。 - Phase 3:异步影子对手 / ghost replay,但仍不保存原始音频,只保存低维派生曲线或聚合指标。 -- Phase 4:实时多人对战协议,需要独立同步模型、房间服务、延迟补偿、断线恢复与更严格反作弊;不应混入 MVP。 +- Phase 4:实时多人对战协议,需要独立同步模型、房间服务、延迟补偿、断线恢复与更严格反作弊;不应混入第二阶段平台作品闭环。 + +## 2.3 Phase 2 技术实施顺序 + +Phase 2 按“契约和领域规则先行,然后最小纵切,再扩展投影”的顺序实施,避免前端 mock 堆积、后端孤岛或排行榜 UI 先行。 + +1. 契约与领域规则:补 `shared-contracts` DTO、`module-bark-battle` 纯领域规则、`rulesetVersion` / `difficultyPreset` / score adjudication,并先写单测。 +2. SpacetimeDB 表与 reducer + api-server BFF:落草稿/config/发布态 config、runtime run start / finish、score record、leaderboard entry、work stats projection、personal summary projection、`migration.rs` 与绑定生成。 +3. 最小前端纵切:接创作入口、单页轻配置表单、发布到稳定 workId、作品详情 CTA、runtime 拉 config、start / finish 串通、结算展示 `serverResult`。 +4. 投影与列表体验:接排行榜、个人历史最近记录 + 最佳摘要、作品统计、我的作品/广场卡片适配。 +5. 收口验证:把 BDD 场景落到测试,执行编码检查、后端 `/healthz` + API smoke、前端人工验收路径,并更新 README/文档。 ## 3. DDD 分层设计 @@ -90,7 +144,7 @@ frontend/runtime - 定义配置版本兼容规则。 - 计算提交结果的派生分数区间与胜负判定是否自洽。 - 计算 `ScoreSummary`、排行榜排序分数、统计指标。 -- 定义反作弊基础规则:时长范围、有效叫声次数上限、峰值范围、能量范围、提交窗口、run 状态机。 +- 定义反作弊基础规则:时长范围、有效声浪触发次数上限、峰值范围、能量范围、提交窗口、run 状态机。 不职责: diff --git a/packages/shared/src/contracts/barkBattle.test.ts b/packages/shared/src/contracts/barkBattle.test.ts new file mode 100644 index 00000000..ae2822c4 --- /dev/null +++ b/packages/shared/src/contracts/barkBattle.test.ts @@ -0,0 +1,130 @@ +import { describe, expect, test } from 'vitest'; + +import { + BARK_BATTLE_DIFFICULTY_PRESETS, + type BarkBattleDraftConfig, + type BarkBattleFinishResponse, + type BarkBattlePersonalBestSummary, + type BarkBattleWorkStats, +} from './barkBattle'; + +describe('Bark Battle shared contracts', () => { + test('default draft config fixture uses normal difficulty and camelCase fields', () => { + const draft: BarkBattleDraftConfig = { + draftId: 'draft-bark-1', + title: '汪汪声浪挑战', + description: '轻配置草稿', + themePreset: 'city-park', + playerDogSkinPreset: 'corgi', + opponentDogSkinPreset: 'husky', + difficultyPreset: 'normal', + leaderboardEnabled: true, + updatedAt: '2026-05-13T03:00:00.000Z', + }; + + expect(BARK_BATTLE_DIFFICULTY_PRESETS).toEqual(['easy', 'normal', 'hard']); + expect(draft.difficultyPreset).toBe('normal'); + expect(Object.keys(draft)).toEqual([ + 'draftId', + 'title', + 'description', + 'themePreset', + 'playerDogSkinPreset', + 'opponentDogSkinPreset', + 'difficultyPreset', + 'leaderboardEnabled', + 'updatedAt', + ]); + }); + + test('finish accepted player_win fixture exposes backend adjudication result', () => { + const response: BarkBattleFinishResponse = { + status: 'accepted', + runId: 'run-bark-1', + workId: 'work-bark-1', + configVersion: 3, + rulesetVersion: 'bark-battle-ruleset-v1', + difficultyPreset: 'hard', + serverResult: 'player_win', + scoreSummary: { + finalEnergy: 87, + triggerCount: 42, + maxVolume: 0.96, + averageVolume: 0.61, + comboMax: 9, + durationMs: 30000, + }, + leaderboardScore: 870429630, + antiCheatFlags: [], + updatedAt: '2026-05-13T03:00:30.000Z', + }; + + expect(response.status).toBe('accepted'); + expect(response.serverResult).toBe('player_win'); + expect(response.scoreSummary.finalEnergy).toBe(87); + expect(response.antiCheatFlags).toEqual([]); + }); + + test('work stats fixture tracks starts, finishes, result counts, flags and energy summary', () => { + const stats: BarkBattleWorkStats = { + workId: 'work-bark-1', + configVersion: 3, + rulesetVersion: 'bark-battle-ruleset-v1', + difficultyPreset: 'normal', + playStartCount: 18, + finishCount: 15, + winCount: 8, + drawCount: 2, + lossCount: 5, + flaggedCount: 1, + leaderboardEntryCount: 7, + bestLeaderboardScore: 930389410, + bestFinalEnergy: 93, + averageFinalEnergy: 41.25, + updatedAt: '2026-05-13T04:00:00.000Z', + }; + + expect(stats.playStartCount).toBe(18); + expect(stats.finishCount).toBe(15); + expect(stats.winCount + stats.drawCount + stats.lossCount).toBe(15); + expect(stats.flaggedCount).toBe(1); + expect(stats.bestFinalEnergy).toBeGreaterThan(stats.averageFinalEnergy); + }); + + test('optional score fields may be omitted instead of serialized as null', () => { + const finishWithoutLeaderboard: BarkBattleFinishResponse = { + status: 'accepted', + runId: 'run-bark-no-rank', + workId: 'work-bark-1', + configVersion: 3, + rulesetVersion: 'bark-battle-ruleset-v1', + difficultyPreset: 'normal', + serverResult: 'draw', + scoreSummary: { + finalEnergy: 50, + triggerCount: 12, + maxVolume: 0.7, + averageVolume: 0.5, + comboMax: 3, + durationMs: 30000, + }, + antiCheatFlags: [], + updatedAt: '2026-05-13T03:00:30.000Z', + }; + const personalBestWithoutWin: BarkBattlePersonalBestSummary = { + workId: 'work-bark-1', + rulesetVersion: 'bark-battle-ruleset-v1', + difficultyPreset: 'normal', + winCount: 0, + drawCount: 1, + lossCount: 2, + finishCount: 3, + updatedAt: '2026-05-13T04:00:00.000Z', + }; + + expect('leaderboardScore' in finishWithoutLeaderboard).toBe(false); + expect('bestLeaderboardScore' in personalBestWithoutWin).toBe(false); + expect('bestFinalEnergy' in personalBestWithoutWin).toBe(false); + }); + +}); diff --git a/packages/shared/src/contracts/barkBattle.ts b/packages/shared/src/contracts/barkBattle.ts new file mode 100644 index 00000000..14449115 --- /dev/null +++ b/packages/shared/src/contracts/barkBattle.ts @@ -0,0 +1,218 @@ +export const BARK_BATTLE_DIFFICULTY_PRESETS = [ + 'easy', + 'normal', + 'hard', +] as const; + +export type BarkBattleDifficultyPreset = + (typeof BARK_BATTLE_DIFFICULTY_PRESETS)[number]; + +export type BarkBattleServerResult = 'player_win' | 'opponent_win' | 'draw'; + +export type BarkBattleFinishStatus = + | 'accepted' + | 'accepted_with_flags' + | 'rejected'; + +export type BarkBattlePlayTypeId = 'bark-battle'; + +export interface BarkBattleConfigEditorPayload { + title: string; + description?: string; + themePreset: string; + playerDogSkinPreset: string; + opponentDogSkinPreset: string; + difficultyPreset: BarkBattleDifficultyPreset; + leaderboardEnabled: boolean; +} + +export interface BarkBattleDraftCreateRequest extends BarkBattleConfigEditorPayload {} + +export interface BarkBattleWorkPublishRequest { + draftId: string; + workId?: string; + publishedSnapshot?: BarkBattleConfigEditorPayload; +} + +export interface BarkBattleDraftConfig extends BarkBattleConfigEditorPayload { + draftId: string; + workId?: string; + configVersion?: number; + rulesetVersion?: string; + updatedAt: string; +} + +export interface BarkBattlePublishedConfig { + workId: string; + draftId?: string | null; + configVersion: number; + rulesetVersion: string; + playTypeId: BarkBattlePlayTypeId; + title: string; + description?: string; + themePreset: string; + playerDogSkinPreset: string; + opponentDogSkinPreset: string; + difficultyPreset: BarkBattleDifficultyPreset; + leaderboardEnabled: boolean; + updatedAt: string; + publishedAt: string; +} + +export interface BarkBattleRuntimeConfig { + workId: string; + configVersion: number; + rulesetVersion: string; + playTypeId: BarkBattlePlayTypeId; + durationMs: number; + energyMin: number; + energyMax: number; + drawThreshold: number; + minBarkGapMs: number; + difficultyPreset: BarkBattleDifficultyPreset; + themePreset: string; + playerDogSkinPreset: string; + opponentDogSkinPreset: string; + leaderboardEnabled: boolean; + updatedAt: string; +} + +export interface BarkBattleRunStartRequest { + workId: string; + configVersion?: number; + sourceRoute?: string; + clientRuntimeVersion?: string; +} + +export interface BarkBattleRunStartResponse { + runId: string; + runToken: string; + workId: string; + configVersion: number; + rulesetVersion: string; + difficultyPreset: BarkBattleDifficultyPreset; + runtimeConfig: BarkBattleRuntimeConfig; + serverStartedAt: string; + expiresAt: string; +} + +export interface BarkBattleDerivedMetrics { + triggerCount: number; + maxVolume: number; + averageVolume: number; + finalEnergy: number; + comboMax: number; +} + +export interface BarkBattleRunFinishRequest { + runId: string; + runToken: string; + workId: string; + configVersion: number; + rulesetVersion: string; + difficultyPreset: BarkBattleDifficultyPreset; + clientStartedAt: string; + clientFinishedAt: string; + durationMs: number; + derivedMetrics: BarkBattleDerivedMetrics; + clientResult?: BarkBattleServerResult; + sampleDigest?: string; + clientRuntimeVersion?: string; +} + +export interface BarkBattleScoreSummary extends BarkBattleDerivedMetrics { + durationMs: number; +} + +export interface BarkBattleFinishResponse { + status: BarkBattleFinishStatus; + runId: string; + workId: string; + configVersion: number; + rulesetVersion: string; + difficultyPreset: BarkBattleDifficultyPreset; + serverResult: BarkBattleServerResult; + scoreSummary: BarkBattleScoreSummary; + leaderboardScore?: number; + antiCheatFlags: string[]; + updatedAt: string; +} + +export interface BarkBattleLeaderboardEntry { + rank: number; + runId: string; + workId: string; + configVersion: number; + rulesetVersion: string; + difficultyPreset: BarkBattleDifficultyPreset; + displayName: string; + serverResult: BarkBattleServerResult; + scoreSummary: BarkBattleScoreSummary; + leaderboardScore: number; + updatedAt: string; +} + +export interface BarkBattleLeaderboardResponse { + workId: string; + configVersion?: number; + rulesetVersion: string; + difficultyPreset: BarkBattleDifficultyPreset; + entries: BarkBattleLeaderboardEntry[]; + viewerBest?: BarkBattleLeaderboardEntry | null; + updatedAt: string; +} + +export interface BarkBattlePersonalHistoryItem { + runId: string; + workId: string; + configVersion: number; + rulesetVersion: string; + difficultyPreset: BarkBattleDifficultyPreset; + serverResult: BarkBattleServerResult; + scoreSummary: BarkBattleScoreSummary; + leaderboardScore?: number; + antiCheatFlags: string[]; + updatedAt: string; +} + +export interface BarkBattlePersonalBestSummary { + workId: string; + configVersion?: number; + rulesetVersion: string; + difficultyPreset: BarkBattleDifficultyPreset; + bestLeaderboardScore?: number; + bestFinalEnergy?: number; + bestTriggerCount?: number; + bestMaxVolume?: number; + winCount: number; + drawCount: number; + lossCount: number; + finishCount: number; + updatedAt: string; +} + +export interface BarkBattlePersonalHistoryResponse { + workId?: string; + difficultyPreset?: BarkBattleDifficultyPreset; + items: BarkBattlePersonalHistoryItem[]; + bestSummary?: BarkBattlePersonalBestSummary | null; + updatedAt: string; +} + +export interface BarkBattleWorkStats { + workId: string; + configVersion?: number; + rulesetVersion: string; + difficultyPreset: BarkBattleDifficultyPreset; + playStartCount: number; + finishCount: number; + winCount: number; + drawCount: number; + lossCount: number; + flaggedCount: number; + leaderboardEntryCount: number; + bestLeaderboardScore?: number; + bestFinalEnergy?: number; + averageFinalEnergy?: number; + updatedAt: string; +} diff --git a/packages/shared/src/contracts/index.ts b/packages/shared/src/contracts/index.ts index fade9a2a..105b187b 100644 --- a/packages/shared/src/contracts/index.ts +++ b/packages/shared/src/contracts/index.ts @@ -3,3 +3,4 @@ export type * from './creationAudio'; export type * from './hyper3d'; export type * from './puzzleCreativeTemplate'; export type * from './visualNovel'; +export type * from './barkBattle'; diff --git a/server-rs/Cargo.lock b/server-rs/Cargo.lock index c8cc837b..206e9802 100644 --- a/server-rs/Cargo.lock +++ b/server-rs/Cargo.lock @@ -89,6 +89,7 @@ dependencies = [ "module-ai", "module-assets", "module-auth", + "module-bark-battle", "module-big-fish", "module-combat", "module-creative-agent", @@ -1764,6 +1765,14 @@ dependencies = [ "tracing", ] +[[package]] +name = "module-bark-battle" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "module-big-fish" version = "0.1.0" @@ -3135,6 +3144,7 @@ dependencies = [ "log", "module-ai", "module-assets", + "module-bark-battle", "module-big-fish", "module-combat", "module-custom-world", @@ -3150,6 +3160,7 @@ dependencies = [ "module-story", "serde", "serde_json", + "sha2", "shared-kernel", "spacetimedb", "spacetimedb-lib", diff --git a/server-rs/Cargo.toml b/server-rs/Cargo.toml index dc9c8b92..614c6cdd 100644 --- a/server-rs/Cargo.toml +++ b/server-rs/Cargo.toml @@ -11,6 +11,7 @@ members = [ "crates/module-ai", "crates/module-assets", "crates/module-auth", + "crates/module-bark-battle", "crates/module-big-fish", "crates/module-combat", "crates/module-creative-agent", @@ -50,6 +51,7 @@ license = "UNLICENSED" module-ai = { path = "crates/module-ai", default-features = false } module-assets = { path = "crates/module-assets", default-features = false } module-auth = { path = "crates/module-auth", default-features = false } +module-bark-battle = { path = "crates/module-bark-battle", default-features = false } module-big-fish = { path = "crates/module-big-fish", default-features = false } module-combat = { path = "crates/module-combat", default-features = false } module-creative-agent = { path = "crates/module-creative-agent", default-features = false } diff --git a/server-rs/crates/api-server/Cargo.toml b/server-rs/crates/api-server/Cargo.toml index 9dec401d..aebc30a3 100644 --- a/server-rs/crates/api-server/Cargo.toml +++ b/server-rs/crates/api-server/Cargo.toml @@ -16,6 +16,7 @@ module-ai = { workspace = true } module-assets = { workspace = true, features = ["server-service"] } module-auth = { workspace = true } module-big-fish = { workspace = true } +module-bark-battle = { workspace = true } module-combat = { workspace = true } module-creative-agent = { workspace = true } module-custom-world = { workspace = true } diff --git a/server-rs/crates/api-server/src/app.rs b/server-rs/crates/api-server/src/app.rs index 048c8721..71458284 100644 --- a/server-rs/crates/api-server/src/app.rs +++ b/server-rs/crates/api-server/src/app.rs @@ -34,6 +34,10 @@ use crate::{ auth_me::auth_me, auth_public_user::{get_public_user_by_code, get_public_user_by_id}, auth_sessions::auth_sessions, + bark_battle::{ + create_bark_battle_draft, finish_bark_battle_run, get_bark_battle_run, + get_bark_battle_runtime_config, publish_bark_battle_work, start_bark_battle_run, + }, big_fish::{ create_big_fish_session, delete_big_fish_work, execute_big_fish_action, get_big_fish_run, get_big_fish_session, get_big_fish_works, list_big_fish_gallery, @@ -1001,6 +1005,48 @@ pub fn build_router(state: AppState) -> Router { require_bearer_auth, )), ) + .route( + "/api/creation/bark-battle/drafts", + post(create_bark_battle_draft).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/creation/bark-battle/works/publish", + post(publish_bark_battle_work).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/bark-battle/works/{work_id}/config", + get(get_bark_battle_runtime_config).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/bark-battle/works/{work_id}/runs", + post(start_bark_battle_run).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/bark-battle/runs/{run_id}", + get(get_bark_battle_run).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/bark-battle/runs/{run_id}/finish", + post(finish_bark_battle_run).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) .route( "/api/creation/square-hole/sessions", post(create_square_hole_agent_session).route_layer(middleware::from_fn_with_state( diff --git a/server-rs/crates/api-server/src/bark_battle.rs b/server-rs/crates/api-server/src/bark_battle.rs new file mode 100644 index 00000000..34bb66db --- /dev/null +++ b/server-rs/crates/api-server/src/bark_battle.rs @@ -0,0 +1,776 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use axum::{ + Json, + extract::{Extension, Path, State, rejection::JsonRejection}, + http::{HeaderName, StatusCode, header}, + response::Response, +}; +use module_bark_battle::{BARK_BATTLE_RULESET_VERSION_V1, BarkBattleRuleset}; +use serde::Deserialize; +use serde_json::{Value, json}; +use shared_contracts::bark_battle::{ + BarkBattleConfigEditorPayload, BarkBattleDerivedMetrics, BarkBattleDifficultyPreset, + BarkBattleDraftConfig, BarkBattleDraftCreateRequest, BarkBattleFinishStatus, + BarkBattlePublishedConfig, BarkBattleRunFinishRequest, BarkBattleRunFinishResponse, + BarkBattleRunStartRequest, BarkBattleRunStartResponse, BarkBattleScoreSummary, + BarkBattleServerResult, BarkBattleWorkPublishRequest, +}; +use shared_kernel::{ + build_prefixed_uuid_id, format_rfc3339, format_timestamp_micros, + offset_datetime_to_unix_micros, parse_rfc3339, +}; +use spacetime_client::{ + BarkBattleDraftCreateRecordInput, BarkBattleRunFinishRecordInput, BarkBattleRunRecord, + BarkBattleRunStartRecordInput, BarkBattleWorkPublishRecordInput, SpacetimeClientError, +}; +use time::{Duration as TimeDuration, OffsetDateTime}; + +use crate::{ + api_response::json_success_body, + auth::AuthenticatedAccessToken, + http_error::AppError, + request_context::RequestContext, + state::AppState, + work_play_tracking::{WorkPlayTrackingDraft, record_work_play_start_after_success}, +}; + +const BARK_BATTLE_RUNTIME_PROVIDER: &str = "bark-battle-runtime"; +const BARK_BATTLE_DRAFT_ID_PREFIX: &str = "bark-battle-draft-"; +const BARK_BATTLE_WORK_ID_PREFIX: &str = "bark-battle-work-"; +const BARK_BATTLE_RUN_ID_PREFIX: &str = "bark-battle-run-"; +const BARK_BATTLE_RUN_TOKEN_PREFIX: &str = "bark-battle-token-"; +const BARK_BATTLE_PLAY_TYPE_ID: &str = "bark-battle"; +const BARK_BATTLE_RUN_TTL_SECONDS: i64 = 10 * 60; + +#[derive(Clone, Debug, Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +struct BarkBattleRunSnapshotRecord { + run_id: String, + work_id: String, + config_version: u64, + ruleset_version: String, + difficulty_preset: String, + #[serde(default)] + client_started_at_micros: i64, + #[serde(default)] + server_started_at_micros: i64, + #[serde(default)] + server_finished_at_micros: Option, + #[serde(default)] + metrics_json: String, + #[serde(default)] + server_result: Option, + #[serde(default)] + validation_status: String, + #[serde(default)] + anti_cheat_flags_json: String, + #[serde(default)] + leaderboard_score: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct BarkBattleDraftConfigSnapshotRecord { + draft_id: String, + #[allow(dead_code)] + work_id: String, + #[allow(dead_code)] + config_version: u64, + #[allow(dead_code)] + ruleset_version: String, + #[serde(default)] + config_json: String, + updated_at_micros: i64, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct BarkBattleRuntimeConfigSnapshotRecord { + work_id: String, + source_draft_id: Option, + config_version: u64, + ruleset_version: String, + #[serde(default)] + config_json: String, + published_at_micros: i64, + updated_at_micros: i64, +} + +pub async fn create_bark_battle_draft( + State(state): State, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let Json(payload) = bark_battle_json(payload, &request_context)?; + let now = current_utc_micros(); + let draft = state + .spacetime_client() + .create_bark_battle_draft(BarkBattleDraftCreateRecordInput { + draft_id: build_prefixed_uuid_id(BARK_BATTLE_DRAFT_ID_PREFIX), + owner_user_id: authenticated.claims().user_id().to_string(), + work_id: build_prefixed_uuid_id(BARK_BATTLE_WORK_ID_PREFIX), + title: Some(payload.title), + description: payload.description, + theme_preset: payload.theme_preset, + player_dog_skin_preset: payload.player_dog_skin_preset, + opponent_dog_skin_preset: payload.opponent_dog_skin_preset, + difficulty_preset: Some( + difficulty_to_spacetime_string(&payload.difficulty_preset).to_string(), + ), + leaderboard_enabled: Some(payload.leaderboard_enabled), + editor_state_json: Some("{}".to_string()), + created_at_micros: now, + }) + .await + .map_err(|error| { + bark_battle_error_response(&request_context, map_bark_battle_client_error(error)) + })?; + let draft = map_draft_config_record(draft, &request_context)?; + Ok(json_success_body(Some(&request_context), draft)) +} + +pub async fn publish_bark_battle_work( + State(state): State, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let Json(payload) = bark_battle_json(payload, &request_context)?; + ensure_non_empty(&request_context, &payload.draft_id, "draftId")?; + let work_id = payload + .work_id + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToString::to_string) + .unwrap_or_else(|| build_prefixed_uuid_id(BARK_BATTLE_WORK_ID_PREFIX)); + let published_snapshot_json = payload + .published_snapshot + .as_ref() + .map(serde_json::to_string) + .transpose() + .map_err(|error| { + bark_battle_error_response( + &request_context, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": BARK_BATTLE_RUNTIME_PROVIDER, + "message": format!("publishedSnapshot JSON 序列化失败: {error}"), + })), + ) + })?; + let published = state + .spacetime_client() + .publish_bark_battle_work(BarkBattleWorkPublishRecordInput { + draft_id: payload.draft_id, + owner_user_id: authenticated.claims().user_id().to_string(), + work_id, + published_snapshot_json, + published_at_micros: current_utc_micros(), + }) + .await + .map_err(|error| { + bark_battle_error_response(&request_context, map_bark_battle_client_error(error)) + })?; + let published = map_published_config_record(published, &request_context)?; + Ok(json_success_body(Some(&request_context), published)) +} + +pub async fn get_bark_battle_runtime_config( + State(state): State, + Path(work_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, +) -> Result, Response> { + ensure_non_empty(&request_context, &work_id, "workId")?; + + let config = state + .spacetime_client() + .get_bark_battle_runtime_config(work_id, Some(authenticated.claims().user_id().to_string())) + .await + .map_err(|error| { + bark_battle_error_response(&request_context, map_bark_battle_client_error(error)) + })?; + let config = map_runtime_config_record(config, &request_context)?; + + Ok(json_success_body(Some(&request_context), config)) +} + +pub async fn start_bark_battle_run( + State(state): State, + Path(work_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let maybe_payload = payload.ok().map(|Json(payload)| payload); + let request = maybe_payload.unwrap_or_else(|| BarkBattleRunStartRequest { + work_id: work_id.clone(), + config_version: None, + source_route: None, + client_runtime_version: None, + }); + let work_id = if request.work_id.trim().is_empty() { + work_id + } else { + request.work_id.trim().to_string() + }; + ensure_non_empty(&request_context, &work_id, "workId")?; + + let owner_user_id = authenticated.claims().user_id().to_string(); + let runtime_config = state + .spacetime_client() + .get_bark_battle_runtime_config(work_id.clone(), Some(owner_user_id.clone())) + .await + .map_err(|error| { + bark_battle_error_response(&request_context, map_bark_battle_client_error(error)) + })?; + let runtime_config = map_runtime_config_record(runtime_config, &request_context)?; + if !request.work_id.trim().is_empty() && request.work_id.trim() != work_id { + return Err(bark_battle_bad_request( + &request_context, + "workId 与路径参数不一致", + )); + } + + if let Some(expected_version) = request.config_version { + if expected_version != runtime_config.config_version { + return Err(bark_battle_bad_request( + &request_context, + "configVersion 与已发布配置不一致", + )); + } + } + + let client_started_at_micros = current_utc_micros(); + let run_token = build_prefixed_uuid_id(BARK_BATTLE_RUN_TOKEN_PREFIX); + let run = state + .spacetime_client() + .start_bark_battle_run(BarkBattleRunStartRecordInput { + run_id: build_prefixed_uuid_id(BARK_BATTLE_RUN_ID_PREFIX), + run_token: run_token.clone(), + owner_user_id: owner_user_id.clone(), + work_id: work_id.clone(), + config_version: u64::from(runtime_config.config_version), + ruleset_version: runtime_config.ruleset_version.clone(), + difficulty_preset: difficulty_to_spacetime_string(&runtime_config.difficulty_preset) + .to_string(), + client_started_at_micros, + server_started_at_micros: client_started_at_micros, + }) + .await + .map_err(|error| { + bark_battle_error_response(&request_context, map_bark_battle_client_error(error)) + })?; + let run_snapshot = parse_run_record(run, &request_context)?; + + record_work_play_start_after_success( + &state, + &request_context, + WorkPlayTrackingDraft::new( + BARK_BATTLE_PLAY_TYPE_ID, + work_id.clone(), + &authenticated, + "/api/runtime/bark-battle/...", + ) + .extra(json!({ + "runId": run_snapshot.run_id, + "workId": work_id, + "configVersion": runtime_config.config_version, + "rulesetVersion": runtime_config.ruleset_version, + "difficultyPreset": runtime_config.difficulty_preset, + "sourceRoute": request.source_route, + "clientRuntimeVersion": request.client_runtime_version, + })), + ) + .await; + + let server_started_at = format_timestamp_micros(run_snapshot.server_started_at_micros); + let expires_at = format_timestamp_micros( + run_snapshot + .server_started_at_micros + .saturating_add(BARK_BATTLE_RUN_TTL_SECONDS * 1_000_000), + ); + + Ok(json_success_body( + Some(&request_context), + BarkBattleRunStartResponse { + run_id: run_snapshot.run_id, + run_token, + work_id: run_snapshot.work_id, + config_version: runtime_config.config_version, + ruleset_version: runtime_config.ruleset_version.clone(), + difficulty_preset: runtime_config.difficulty_preset.clone(), + runtime_config, + server_started_at, + expires_at, + }, + )) +} + +pub async fn get_bark_battle_run( + State(state): State, + Path(run_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, +) -> Result, Response> { + ensure_non_empty(&request_context, &run_id, "runId")?; + let run = state + .spacetime_client() + .get_bark_battle_run(run_id, authenticated.claims().user_id().to_string()) + .await + .map_err(|error| { + bark_battle_error_response(&request_context, map_bark_battle_client_error(error)) + })?; + let run = parse_run_record(run, &request_context)?; + + Ok(json_success_body(Some(&request_context), run)) +} + +pub async fn finish_bark_battle_run( + State(state): State, + Path(run_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let Json(payload) = bark_battle_json(payload, &request_context)?; + ensure_non_empty(&request_context, &run_id, "runId")?; + ensure_non_empty(&request_context, &payload.work_id, "workId")?; + ensure_non_empty(&request_context, &payload.run_token, "runToken")?; + if payload.run_id != run_id { + return Err(bark_battle_bad_request( + &request_context, + "runId 与路径参数不一致", + )); + } + if payload.ruleset_version != BARK_BATTLE_RULESET_VERSION_V1 { + return Err(bark_battle_bad_request( + &request_context, + "rulesetVersion 不支持", + )); + } + + let client_finished_at_micros = parse_client_time_to_micros(&payload.client_finished_at) + .map_err(|message| bark_battle_bad_request(&request_context, &message))?; + let derived = &payload.derived_metrics; + let opponent_final_energy = derive_server_opponent_final_energy(derived); + let metrics_json = serde_json::to_string(&json!({ + "clientStartedAt": payload.client_started_at, + "clientFinishedAt": payload.client_finished_at, + "durationMs": payload.duration_ms, + "derivedMetrics": payload.derived_metrics, + "clientResult": payload.client_result, + "sampleDigest": payload.sample_digest, + "clientRuntimeVersion": payload.client_runtime_version, + })) + .unwrap_or_else(|_| "{}".to_string()); + let derived_metrics_json = serde_json::to_string(derived).unwrap_or_else(|_| "{}".to_string()); + + let run = state + .spacetime_client() + .finish_bark_battle_run(BarkBattleRunFinishRecordInput { + run_id, + run_token: payload.run_token, + owner_user_id: authenticated.claims().user_id().to_string(), + work_id: payload.work_id.clone(), + config_version: u64::from(payload.config_version), + ruleset_version: payload.ruleset_version.clone(), + difficulty_preset: difficulty_to_spacetime_string(&payload.difficulty_preset) + .to_string(), + client_finished_at_micros, + server_finished_at_micros: current_utc_micros(), + duration_ms: payload.duration_ms, + trigger_count: u64::from(derived.trigger_count), + max_volume_millis: unit_to_millis(derived.max_volume), + average_volume_millis: unit_to_millis(derived.average_volume), + final_energy_millis: energy_to_millis(derived.final_energy), + opponent_final_energy_millis: energy_to_millis(opponent_final_energy), + max_combo: derived.combo_max, + metrics_json, + derived_metrics_json, + }) + .await + .map_err(|error| { + bark_battle_error_response(&request_context, map_bark_battle_client_error(error)) + })?; + let run = parse_run_record(run, &request_context)?; + + Ok(json_success_body( + Some(&request_context), + map_finish_response(run, &payload.derived_metrics), + )) +} + +fn map_finish_response( + run: BarkBattleRunSnapshotRecord, + fallback_metrics: &BarkBattleDerivedMetrics, +) -> BarkBattleRunFinishResponse { + let score_summary = + parse_score_summary(&run.metrics_json).unwrap_or_else(|| BarkBattleScoreSummary { + duration_ms: 0, + trigger_count: fallback_metrics.trigger_count, + max_volume: fallback_metrics.max_volume, + average_volume: fallback_metrics.average_volume, + final_energy: fallback_metrics.final_energy, + combo_max: fallback_metrics.combo_max, + }); + BarkBattleRunFinishResponse { + status: parse_finish_status(&run.validation_status), + run_id: run.run_id, + work_id: run.work_id, + config_version: run.config_version.min(u64::from(u32::MAX)) as u32, + ruleset_version: run.ruleset_version, + difficulty_preset: parse_difficulty_lossy(&run.difficulty_preset), + server_result: parse_server_result_lossy(run.server_result.as_deref()), + score_summary, + leaderboard_score: run.leaderboard_score, + anti_cheat_flags: parse_string_vec(&run.anti_cheat_flags_json), + updated_at: format_timestamp_micros( + run.server_finished_at_micros + .unwrap_or(run.server_started_at_micros), + ), + } +} + +fn parse_run_record( + value: BarkBattleRunRecord, + request_context: &RequestContext, +) -> Result { + serde_json::from_value(value).map_err(|error| { + bark_battle_error_response( + request_context, + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": BARK_BATTLE_RUNTIME_PROVIDER, + "message": format!("Bark Battle run JSON 解析失败: {error}"), + })), + ) + }) +} + +fn parse_draft_snapshot_record( + value: Value, + request_context: &RequestContext, +) -> Result { + serde_json::from_value(value) + .map_err(|error| bark_battle_snapshot_parse_error(request_context, "draft config", error)) +} + +fn parse_runtime_snapshot_record( + value: Value, + request_context: &RequestContext, +) -> Result { + serde_json::from_value(value) + .map_err(|error| bark_battle_snapshot_parse_error(request_context, "runtime config", error)) +} + +fn map_draft_config_record( + value: Value, + request_context: &RequestContext, +) -> Result { + let snapshot = parse_draft_snapshot_record(value, request_context)?; + let editor_config = parse_editor_config_record(&snapshot.config_json, request_context)?; + Ok(BarkBattleDraftConfig { + draft_id: snapshot.draft_id, + title: editor_config.title, + description: editor_config.description, + theme_preset: editor_config.theme_preset, + player_dog_skin_preset: editor_config.player_dog_skin_preset, + opponent_dog_skin_preset: editor_config.opponent_dog_skin_preset, + difficulty_preset: editor_config.difficulty_preset, + leaderboard_enabled: editor_config.leaderboard_enabled, + updated_at: format_timestamp_micros(snapshot.updated_at_micros), + }) +} + +fn map_runtime_config_record( + value: Value, + request_context: &RequestContext, +) -> Result { + let snapshot = parse_runtime_snapshot_record(value, request_context)?; + let editor_config = parse_editor_config_record(&snapshot.config_json, request_context)?; + let ruleset = BarkBattleRuleset::v1(); + Ok(shared_contracts::bark_battle::BarkBattleRuntimeConfig { + work_id: snapshot.work_id, + config_version: snapshot.config_version.min(u64::from(u32::MAX)) as u32, + ruleset_version: snapshot.ruleset_version, + play_type_id: BARK_BATTLE_PLAY_TYPE_ID.to_string(), + duration_ms: ruleset.standard_duration_ms, + energy_min: 0.0, + energy_max: 100.0, + draw_threshold: ruleset.draw_threshold_energy as f32, + min_bark_gap_ms: ruleset.min_bark_gap_ms, + difficulty_preset: editor_config.difficulty_preset, + theme_preset: editor_config.theme_preset, + player_dog_skin_preset: editor_config.player_dog_skin_preset, + opponent_dog_skin_preset: editor_config.opponent_dog_skin_preset, + leaderboard_enabled: editor_config.leaderboard_enabled, + updated_at: format_timestamp_micros(snapshot.updated_at_micros), + }) +} + +fn map_published_config_record( + value: Value, + request_context: &RequestContext, +) -> Result { + let snapshot = parse_runtime_snapshot_record(value, request_context)?; + let editor_config = parse_editor_config_record(&snapshot.config_json, request_context)?; + Ok(BarkBattlePublishedConfig { + work_id: snapshot.work_id, + draft_id: snapshot.source_draft_id, + config_version: snapshot.config_version.min(u64::from(u32::MAX)) as u32, + ruleset_version: snapshot.ruleset_version, + play_type_id: BARK_BATTLE_PLAY_TYPE_ID.to_string(), + title: editor_config.title, + description: editor_config.description, + theme_preset: editor_config.theme_preset, + player_dog_skin_preset: editor_config.player_dog_skin_preset, + opponent_dog_skin_preset: editor_config.opponent_dog_skin_preset, + difficulty_preset: editor_config.difficulty_preset, + leaderboard_enabled: editor_config.leaderboard_enabled, + updated_at: format_timestamp_micros(snapshot.updated_at_micros), + published_at: format_timestamp_micros(snapshot.published_at_micros), + }) +} + +fn parse_editor_config_record( + config_json: &str, + request_context: &RequestContext, +) -> Result { + serde_json::from_str(config_json).map_err(|error| { + bark_battle_error_response( + request_context, + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": BARK_BATTLE_RUNTIME_PROVIDER, + "message": format!("Bark Battle configJson 解析失败: {error}"), + })), + ) + }) +} + +fn bark_battle_snapshot_parse_error( + request_context: &RequestContext, + label: &str, + error: serde_json::Error, +) -> Response { + bark_battle_error_response( + request_context, + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": BARK_BATTLE_RUNTIME_PROVIDER, + "message": format!("Bark Battle {label} JSON 解析失败: {error}"), + })), + ) +} + +fn bark_battle_json( + payload: Result, JsonRejection>, + request_context: &RequestContext, +) -> Result, Response> { + payload.map_err(|error| { + bark_battle_error_response( + request_context, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": BARK_BATTLE_RUNTIME_PROVIDER, + "message": error.body_text(), + })), + ) + }) +} + +fn ensure_non_empty( + request_context: &RequestContext, + value: &str, + field_name: &str, +) -> Result<(), Response> { + if value.trim().is_empty() { + return Err(bark_battle_bad_request( + request_context, + &format!("{field_name} is required"), + )); + } + Ok(()) +} + +fn bark_battle_bad_request(request_context: &RequestContext, message: &str) -> Response { + bark_battle_error_response( + request_context, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": BARK_BATTLE_RUNTIME_PROVIDER, + "message": message, + })), + ) +} + +fn map_bark_battle_client_error(error: SpacetimeClientError) -> AppError { + let status = match &error { + SpacetimeClientError::Runtime(_) => StatusCode::BAD_REQUEST, + SpacetimeClientError::Procedure(message) + if message.contains("不存在") + || message.contains("not found") + || message.contains("does not exist") => + { + StatusCode::NOT_FOUND + } + SpacetimeClientError::Procedure(message) + if message.contains("不能为空") + || message.contains("不匹配") + || message.contains("不支持") + || message.contains("已结束") + || message.contains("已存在") => + { + StatusCode::BAD_REQUEST + } + _ => StatusCode::BAD_GATEWAY, + }; + + AppError::from_status(status).with_details(json!({ + "provider": "spacetimedb", + "message": error.to_string(), + })) +} + +fn bark_battle_error_response(request_context: &RequestContext, error: AppError) -> Response { + let mut response = error.into_response_with_context(Some(request_context)); + response.headers_mut().insert( + HeaderName::from_static("x-genarrative-provider"), + header::HeaderValue::from_static(BARK_BATTLE_RUNTIME_PROVIDER), + ); + response +} + +fn parse_client_time_to_micros(value: &str) -> Result { + let trimmed = value.trim(); + if trimmed.is_empty() { + return Err("client timestamp is required".to_string()); + } + if let Ok(micros) = trimmed.parse::() { + return Ok(micros); + } + parse_rfc3339(trimmed).map(offset_datetime_to_unix_micros) +} + +fn current_utc_micros() -> i64 { + let duration = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default(); + (duration.as_secs() as i64) * 1_000_000 + i64::from(duration.subsec_micros()) +} + +fn unit_to_millis(value: f32) -> u32 { + (value.clamp(0.0, 1.0) * 1_000.0).round() as u32 +} + +fn energy_to_millis(value: f32) -> u32 { + (value.clamp(0.0, 100.0) * 1_000.0).round() as u32 +} + +fn derive_server_opponent_final_energy(metrics: &BarkBattleDerivedMetrics) -> f32 { + let ruleset = BarkBattleRuleset::v1(); + let pressure = (metrics.average_volume * 24.0) + + (metrics.max_volume * 16.0) + + (metrics.trigger_count as f32 * 0.35) + + (metrics.combo_max as f32 * 0.2); + (ruleset.max_final_energy - pressure).clamp(ruleset.min_final_energy, ruleset.max_final_energy) +} + +fn difficulty_to_spacetime_string(value: &BarkBattleDifficultyPreset) -> &'static str { + match value { + BarkBattleDifficultyPreset::Easy => "easy", + BarkBattleDifficultyPreset::Normal => "normal", + BarkBattleDifficultyPreset::Hard => "hard", + } +} + +fn parse_difficulty(value: &str) -> Result { + match value { + "easy" => Ok(BarkBattleDifficultyPreset::Easy), + "normal" => Ok(BarkBattleDifficultyPreset::Normal), + "hard" => Ok(BarkBattleDifficultyPreset::Hard), + _ => Err( + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": BARK_BATTLE_RUNTIME_PROVIDER, + "message": format!("Bark Battle difficultyPreset 不支持: {value}"), + })), + ), + } +} + +fn parse_difficulty_lossy(value: &str) -> BarkBattleDifficultyPreset { + parse_difficulty(value).unwrap_or(BarkBattleDifficultyPreset::Normal) +} + +fn parse_finish_status(value: &str) -> BarkBattleFinishStatus { + match value { + "accepted" => BarkBattleFinishStatus::Accepted, + "accepted_with_flags" => BarkBattleFinishStatus::AcceptedWithFlags, + "rejected" => BarkBattleFinishStatus::Rejected, + _ => BarkBattleFinishStatus::Rejected, + } +} + +fn parse_server_result_lossy(value: Option<&str>) -> BarkBattleServerResult { + match value { + Some("player_win") => BarkBattleServerResult::PlayerWin, + Some("opponent_win") => BarkBattleServerResult::OpponentWin, + Some("draw") => BarkBattleServerResult::Draw, + _ => BarkBattleServerResult::Draw, + } +} + +fn parse_score_summary(metrics_json: &str) -> Option { + let value: Value = serde_json::from_str(metrics_json).ok()?; + let derived = value.get("derivedMetrics")?; + Some(BarkBattleScoreSummary { + duration_ms: value.get("durationMs")?.as_u64()?, + trigger_count: derived + .get("triggerCount")? + .as_u64()? + .min(u64::from(u32::MAX)) as u32, + max_volume: derived.get("maxVolume")?.as_f64()? as f32, + average_volume: derived.get("averageVolume")?.as_f64()? as f32, + final_energy: derived.get("finalEnergy")?.as_f64()? as f32, + combo_max: derived.get("comboMax")?.as_u64()?.min(u64::from(u32::MAX)) as u32, + }) +} + +fn parse_string_vec(value: &str) -> Vec { + serde_json::from_str(value).unwrap_or_default() +} + +#[allow(dead_code)] +fn format_rfc3339_or_timestamp_micros(micros: i64) -> String { + let seconds = micros.div_euclid(1_000_000); + let subsec_micros = micros.rem_euclid(1_000_000); + let Ok(value) = OffsetDateTime::from_unix_timestamp(seconds) + .map(|value| value + TimeDuration::microseconds(subsec_micros)) + else { + return format_timestamp_micros(micros); + }; + format_rfc3339(value).unwrap_or_else(|_| format_timestamp_micros(micros)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unit_and_energy_are_clamped_to_spacetime_millis() { + assert_eq!(unit_to_millis(0.625), 625); + assert_eq!(unit_to_millis(3.0), 1000); + assert_eq!(energy_to_millis(88.456), 88_456); + assert_eq!(energy_to_millis(120.0), 100_000); + } + + #[test] + fn parses_rfc3339_and_numeric_client_timestamps() { + assert_eq!( + parse_client_time_to_micros("1713686401234567").unwrap(), + 1_713_686_401_234_567 + ); + assert_eq!( + parse_client_time_to_micros("2024-04-21T04:00:01.234567Z").unwrap(), + 1_713_672_001_234_567 + ); + } +} diff --git a/server-rs/crates/api-server/src/creation_entry_config.rs b/server-rs/crates/api-server/src/creation_entry_config.rs index 2efa038d..9ade6169 100644 --- a/server-rs/crates/api-server/src/creation_entry_config.rs +++ b/server-rs/crates/api-server/src/creation_entry_config.rs @@ -78,6 +78,9 @@ pub fn resolve_creation_entry_route_id(path: &str) -> Option<&'static str> { if normalized.starts_with("/api/runtime/match3d") { return Some("match3d"); } + if normalized.starts_with("/api/runtime/bark-battle") { + return Some("bark-battle"); + } if normalized.starts_with("/api/runtime/square-hole") { return Some("square-hole"); } @@ -117,6 +120,7 @@ pub(crate) fn test_creation_entry_config_response() test_creation_type("big-fish", false, true, 20), test_creation_type("puzzle", true, true, 30), test_creation_type("match3d", true, true, 40), + test_creation_type("bark-battle", true, true, 45), test_creation_type("square-hole", false, true, 50), test_creation_type("visual-novel", true, false, 60), test_creation_type("airp", true, false, 70), @@ -172,6 +176,10 @@ mod tests { resolve_creation_entry_route_id("/api/creation/visual-novel/sessions"), Some("visual-novel"), ); + assert_eq!( + resolve_creation_entry_route_id("/api/runtime/bark-battle/works/work-1/config"), + Some("bark-battle"), + ); assert_eq!(resolve_creation_entry_route_id("/healthz"), None); } } diff --git a/server-rs/crates/api-server/src/main.rs b/server-rs/crates/api-server/src/main.rs index 5f777f7b..ca423f61 100644 --- a/server-rs/crates/api-server/src/main.rs +++ b/server-rs/crates/api-server/src/main.rs @@ -13,6 +13,7 @@ mod auth_payload; mod auth_public_user; mod auth_session; mod auth_sessions; +mod bark_battle; mod big_fish; mod big_fish_agent_turn; mod big_fish_draft_compiler; diff --git a/server-rs/crates/api-server/src/puzzle.rs b/server-rs/crates/api-server/src/puzzle.rs index 37797e7f..6dd6b374 100644 --- a/server-rs/crates/api-server/src/puzzle.rs +++ b/server-rs/crates/api-server/src/puzzle.rs @@ -57,8 +57,8 @@ use spacetime_client::{ PuzzleAgentMessageRecord, PuzzleAgentMessageSubmitRecordInput, PuzzleAgentSessionCreateRecordInput, PuzzleAgentSessionRecord, PuzzleAgentSuggestedActionRecord, PuzzleAnchorItemRecord, PuzzleAnchorPackRecord, - PuzzleAudioAssetRecord, PuzzleCreatorIntentRecord, PuzzleDraftLevelRecord, PuzzleFormDraftRecord, - PuzzleFormDraftSaveRecordInput, PuzzleGeneratedImageCandidateRecord, + PuzzleAudioAssetRecord, PuzzleCreatorIntentRecord, PuzzleDraftLevelRecord, + PuzzleFormDraftRecord, PuzzleFormDraftSaveRecordInput, PuzzleGeneratedImageCandidateRecord, PuzzleGeneratedImagesSaveRecordInput, PuzzleLeaderboardEntryRecord, PuzzleLeaderboardSubmitRecordInput, PuzzlePublishRecordInput, PuzzleRecommendedNextWorkRecord, PuzzleResultDraftRecord, PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, @@ -2061,7 +2061,9 @@ fn map_puzzle_draft_level_response(level: PuzzleDraftLevelRecord) -> PuzzleDraft level_name: level.level_name, picture_description: level.picture_description, picture_reference: level.picture_reference, - background_music: level.background_music.map(map_puzzle_audio_asset_record_response), + background_music: level + .background_music + .map(map_puzzle_audio_asset_record_response), candidates: level .candidates .into_iter() @@ -2667,7 +2669,9 @@ fn parse_puzzle_level_records_from_module_json( level_name: level.level_name, picture_description: level.picture_description, picture_reference: level.picture_reference, - background_music: level.background_music.map(map_puzzle_audio_asset_domain_record), + background_music: level + .background_music + .map(map_puzzle_audio_asset_domain_record), candidates: level .candidates .into_iter() @@ -4608,8 +4612,7 @@ mod tests { let levels_json = serialize_puzzle_levels_response(&request_context, &[level]) .expect("levels should serialize"); - let payload: Value = - serde_json::from_str(&levels_json).expect("levels json should parse"); + let payload: Value = serde_json::from_str(&levels_json).expect("levels json should parse"); assert_eq!( payload[0]["background_music"]["audio_src"], Value::String("/generated-puzzle-assets/audio.mp3".to_string()) diff --git a/server-rs/crates/module-bark-battle/Cargo.toml b/server-rs/crates/module-bark-battle/Cargo.toml new file mode 100644 index 00000000..d93dd6ab --- /dev/null +++ b/server-rs/crates/module-bark-battle/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "module-bark-battle" +edition.workspace = true +version.workspace = true +license.workspace = true + +[features] +default = [] + +[dependencies] +serde = { workspace = true } + +[dev-dependencies] +serde_json = { workspace = true } diff --git a/server-rs/crates/module-bark-battle/src/domain.rs b/server-rs/crates/module-bark-battle/src/domain.rs new file mode 100644 index 00000000..7436cb5a --- /dev/null +++ b/server-rs/crates/module-bark-battle/src/domain.rs @@ -0,0 +1,162 @@ +use serde::{Deserialize, Serialize}; + +pub const BARK_BATTLE_RULESET_VERSION_V1: &str = "bark-battle-ruleset-v1"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DifficultyPreset { + Easy, + Normal, + Hard, +} + +impl DifficultyPreset { + pub fn ai_preset_key(self) -> &'static str { + match self { + Self::Easy => "bark-battle-ai-easy", + Self::Normal => "bark-battle-ai-normal", + Self::Hard => "bark-battle-ai-hard", + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +pub struct RulesetThresholdsSignature { + pub standard_duration_ms: u64, + pub min_duration_ms: u64, + pub max_duration_ms: u64, + pub min_bark_gap_ms: u64, + pub trigger_count_tolerance: u32, + pub min_volume: f32, + pub max_volume: f32, + pub min_average_volume: f32, + pub max_average_volume: f32, + pub min_final_energy: f32, + pub max_final_energy: f32, + pub min_combo: u32, + pub max_combo: u32, + pub draw_threshold_energy: u32, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BarkBattleRuleset { + pub version: &'static str, + pub difficulty: DifficultyPreset, + pub ai_preset_key: &'static str, + pub standard_duration_ms: u64, + pub min_duration_ms: u64, + pub max_duration_ms: u64, + pub min_bark_gap_ms: u64, + pub trigger_count_tolerance: u32, + pub min_volume: f32, + pub max_volume: f32, + pub min_average_volume: f32, + pub max_average_volume: f32, + pub min_final_energy: f32, + pub max_final_energy: f32, + pub min_combo: u32, + pub max_combo: u32, + pub draw_threshold_energy: u32, +} + +impl BarkBattleRuleset { + pub fn v1() -> Self { + Self::for_difficulty(DifficultyPreset::Normal) + } + + pub fn for_difficulty(difficulty: DifficultyPreset) -> Self { + Self { + version: BARK_BATTLE_RULESET_VERSION_V1, + difficulty, + ai_preset_key: difficulty.ai_preset_key(), + standard_duration_ms: 30_000, + min_duration_ms: 28_000, + max_duration_ms: 35_000, + min_bark_gap_ms: 250, + trigger_count_tolerance: 2, + min_volume: 0.0, + max_volume: 1.0, + min_average_volume: 0.0, + max_average_volume: 1.0, + min_final_energy: 0.0, + max_final_energy: 100.0, + min_combo: 0, + max_combo: 999, + draw_threshold_energy: 3, + } + } + + pub fn thresholds_signature(&self) -> RulesetThresholdsSignature { + RulesetThresholdsSignature { + standard_duration_ms: self.standard_duration_ms, + min_duration_ms: self.min_duration_ms, + max_duration_ms: self.max_duration_ms, + min_bark_gap_ms: self.min_bark_gap_ms, + trigger_count_tolerance: self.trigger_count_tolerance, + min_volume: self.min_volume, + max_volume: self.max_volume, + min_average_volume: self.min_average_volume, + max_average_volume: self.max_average_volume, + min_final_energy: self.min_final_energy, + max_final_energy: self.max_final_energy, + min_combo: self.min_combo, + max_combo: self.max_combo, + draw_threshold_energy: self.draw_threshold_energy, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +pub struct BarkBattleFinishMetrics { + pub duration_ms: u64, + pub trigger_count: u64, + /// 归一化音量,合法范围为 0.0..=1.0。 + pub max_volume: f32, + pub average_volume: f32, + pub final_energy: f32, + pub max_combo: u32, + pub finished_at_micros: i64, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum FinishValidationDecision { + Accepted, + AcceptedWithFlags, + Rejected, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AntiCheatFlag { + DurationTooShort, + DurationTooLong, + TriggerCountTooHigh, + MaxVolumeOutOfRange, + AverageVolumeOutOfRange, + FinalEnergyOutOfRange, + MaxComboOutOfRange, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct FinishValidation { + pub decision: FinishValidationDecision, + pub anti_cheat_flags: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum BattleResult { + PlayerWin, + OpponentWin, + Draw, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct BarkBattleLeaderboardScore { + pub final_energy_millis: u32, + pub trigger_count: u64, + pub max_volume_millis: u32, + pub duration_closeness_ms: u64, + pub finished_at_micros: i64, +} diff --git a/server-rs/crates/module-bark-battle/src/lib.rs b/server-rs/crates/module-bark-battle/src/lib.rs new file mode 100644 index 00000000..b587645a --- /dev/null +++ b/server-rs/crates/module-bark-battle/src/lib.rs @@ -0,0 +1,5 @@ +pub mod domain; +pub mod scoring; + +pub use domain::*; +pub use scoring::*; diff --git a/server-rs/crates/module-bark-battle/src/scoring.rs b/server-rs/crates/module-bark-battle/src/scoring.rs new file mode 100644 index 00000000..12aff9c2 --- /dev/null +++ b/server-rs/crates/module-bark-battle/src/scoring.rs @@ -0,0 +1,316 @@ +use crate::domain::*; + +pub fn validate_finish_metrics( + ruleset: &BarkBattleRuleset, + metrics: &BarkBattleFinishMetrics, +) -> FinishValidation { + let mut flags = Vec::new(); + let mut rejected = false; + + if metrics.duration_ms < ruleset.min_duration_ms { + flags.push(AntiCheatFlag::DurationTooShort); + rejected = true; + } + if metrics.duration_ms > ruleset.max_duration_ms { + flags.push(AntiCheatFlag::DurationTooLong); + rejected = true; + } + + let max_trigger_count = + metrics.duration_ms / ruleset.min_bark_gap_ms + u64::from(ruleset.trigger_count_tolerance); + if metrics.trigger_count > max_trigger_count { + flags.push(AntiCheatFlag::TriggerCountTooHigh); + } + + if !is_in_range(metrics.max_volume, ruleset.min_volume, ruleset.max_volume) { + flags.push(AntiCheatFlag::MaxVolumeOutOfRange); + rejected = true; + } + if !is_in_range( + metrics.average_volume, + ruleset.min_average_volume, + ruleset.max_average_volume, + ) { + flags.push(AntiCheatFlag::AverageVolumeOutOfRange); + rejected = true; + } + if !is_in_range( + metrics.final_energy, + ruleset.min_final_energy, + ruleset.max_final_energy, + ) { + flags.push(AntiCheatFlag::FinalEnergyOutOfRange); + rejected = true; + } + if metrics.max_combo < ruleset.min_combo || metrics.max_combo > ruleset.max_combo { + flags.push(AntiCheatFlag::MaxComboOutOfRange); + rejected = true; + } + + let decision = if rejected { + FinishValidationDecision::Rejected + } else if flags.is_empty() { + FinishValidationDecision::Accepted + } else { + FinishValidationDecision::AcceptedWithFlags + }; + + FinishValidation { + decision, + anti_cheat_flags: flags, + } +} + +pub fn adjudicate_result( + ruleset: &BarkBattleRuleset, + player_final_energy: f32, + opponent_final_energy: f32, +) -> BattleResult { + let delta = player_final_energy - opponent_final_energy; + if delta.abs() <= ruleset.draw_threshold_energy as f32 { + BattleResult::Draw + } else if delta > 0.0 { + BattleResult::PlayerWin + } else { + BattleResult::OpponentWin + } +} + +pub fn compute_leaderboard_score( + ruleset: &BarkBattleRuleset, + metrics: &BarkBattleFinishMetrics, + validation: &FinishValidation, + result: BattleResult, +) -> Option { + if result != BattleResult::PlayerWin + || validation.decision == FinishValidationDecision::Rejected + { + return None; + } + + Some(BarkBattleLeaderboardScore { + final_energy_millis: to_millis(metrics.final_energy), + trigger_count: metrics.trigger_count, + max_volume_millis: to_millis(metrics.max_volume), + duration_closeness_ms: metrics.duration_ms.abs_diff(ruleset.standard_duration_ms), + finished_at_micros: metrics.finished_at_micros, + }) +} + +fn is_in_range(value: f32, min: f32, max: f32) -> bool { + value.is_finite() && value >= min && value <= max +} + +fn to_millis(value: f32) -> u32 { + (value * 1_000.0).round().clamp(0.0, u32::MAX as f32) as u32 +} + +#[cfg(test)] +mod tests { + use crate::*; + + fn metrics(duration_ms: u64) -> BarkBattleFinishMetrics { + BarkBattleFinishMetrics { + duration_ms, + trigger_count: 10, + max_volume: 0.8, + average_volume: 0.6, + final_energy: 60.0, + max_combo: 5, + finished_at_micros: 1_000_000, + } + } + + #[test] + fn serde_uses_contract_snake_case_for_domain_enums() { + assert_eq!( + serde_json::to_value(DifficultyPreset::Easy).expect("serialize difficulty"), + serde_json::json!("easy") + ); + assert_eq!( + serde_json::to_value(FinishValidationDecision::AcceptedWithFlags) + .expect("serialize decision"), + serde_json::json!("accepted_with_flags") + ); + assert_eq!( + serde_json::to_value(AntiCheatFlag::AverageVolumeOutOfRange) + .expect("serialize anti-cheat flag"), + serde_json::json!("average_volume_out_of_range") + ); + assert_eq!( + serde_json::to_value(BattleResult::PlayerWin).expect("serialize battle result"), + serde_json::json!("player_win") + ); + } + + #[test] + fn accepts_duration_inside_28s_to_35s_window() { + let ruleset = BarkBattleRuleset::v1(); + + assert_eq!( + validate_finish_metrics(&ruleset, &metrics(28_000)).decision, + FinishValidationDecision::Accepted + ); + assert_eq!( + validate_finish_metrics(&ruleset, &metrics(35_000)).decision, + FinishValidationDecision::Accepted + ); + } + + #[test] + fn rejects_or_flags_extreme_duration() { + let ruleset = BarkBattleRuleset::v1(); + + assert_ne!( + validate_finish_metrics(&ruleset, &metrics(1_000)).decision, + FinishValidationDecision::Accepted + ); + assert_ne!( + validate_finish_metrics(&ruleset, &metrics(300_000)).decision, + FinishValidationDecision::Accepted + ); + } + + #[test] + fn flags_trigger_count_above_physical_limit_with_tolerance() { + let ruleset = BarkBattleRuleset::v1(); + let mut input = metrics(30_000); + input.trigger_count = input.duration_ms / ruleset.min_bark_gap_ms + + u64::from(ruleset.trigger_count_tolerance) + + 1; + + let validation = validate_finish_metrics(&ruleset, &input); + + assert_eq!( + validation.decision, + FinishValidationDecision::AcceptedWithFlags + ); + assert!( + validation + .anti_cheat_flags + .contains(&AntiCheatFlag::TriggerCountTooHigh) + ); + } + + #[test] + fn rejects_final_energy_outside_range() { + let ruleset = BarkBattleRuleset::v1(); + let mut input = metrics(30_000); + input.final_energy = ruleset.max_final_energy + 0.1; + + let validation = validate_finish_metrics(&ruleset, &input); + + assert_eq!(validation.decision, FinishValidationDecision::Rejected); + assert!( + validation + .anti_cheat_flags + .contains(&AntiCheatFlag::FinalEnergyOutOfRange) + ); + } + + #[test] + fn leaderboard_score_only_for_player_win_and_not_rejected() { + let ruleset = BarkBattleRuleset::v1(); + let input = metrics(30_000); + let validation = validate_finish_metrics(&ruleset, &input); + + assert!( + compute_leaderboard_score(&ruleset, &input, &validation, BattleResult::PlayerWin) + .is_some() + ); + assert!( + compute_leaderboard_score(&ruleset, &input, &validation, BattleResult::Draw).is_none() + ); + assert!( + compute_leaderboard_score(&ruleset, &input, &validation, BattleResult::OpponentWin) + .is_none() + ); + + let rejected = FinishValidation { + decision: FinishValidationDecision::Rejected, + anti_cheat_flags: vec![AntiCheatFlag::DurationTooShort], + }; + assert!( + compute_leaderboard_score(&ruleset, &input, &rejected, BattleResult::PlayerWin) + .is_none() + ); + } + + #[test] + fn adjudicates_draw_threshold_boundaries() { + let ruleset = BarkBattleRuleset::v1(); + assert_eq!(adjudicate_result(&ruleset, 53.0, 50.0), BattleResult::Draw); + assert_eq!( + adjudicate_result(&ruleset, 53.1, 50.0), + BattleResult::PlayerWin + ); + assert_eq!( + adjudicate_result(&ruleset, 46.9, 50.0), + BattleResult::OpponentWin + ); + } + + #[test] + fn validates_inclusive_metric_boundaries_and_rejects_non_finite() { + let ruleset = BarkBattleRuleset::v1(); + let mut input = metrics(30_000); + input.trigger_count = input.duration_ms / ruleset.min_bark_gap_ms + + u64::from(ruleset.trigger_count_tolerance); + input.max_volume = ruleset.min_volume; + input.final_energy = ruleset.max_final_energy; + input.max_combo = ruleset.max_combo; + assert_eq!( + validate_finish_metrics(&ruleset, &input).decision, + FinishValidationDecision::Accepted + ); + + input.max_volume = f32::NAN; + assert_eq!( + validate_finish_metrics(&ruleset, &input).decision, + FinishValidationDecision::Rejected + ); + input.max_volume = 0.8; + input.average_volume = ruleset.max_average_volume + 0.1; + let validation = validate_finish_metrics(&ruleset, &input); + assert_eq!(validation.decision, FinishValidationDecision::Rejected); + assert!( + validation + .anti_cheat_flags + .contains(&AntiCheatFlag::AverageVolumeOutOfRange) + ); + input.average_volume = 0.6; + input.final_energy = f32::INFINITY; + assert_eq!( + validate_finish_metrics(&ruleset, &input).decision, + FinishValidationDecision::Rejected + ); + } + + #[test] + fn leaderboard_score_allows_flagged_but_accepted_player_wins() { + let ruleset = BarkBattleRuleset::v1(); + let input = metrics(30_000); + let validation = FinishValidation { + decision: FinishValidationDecision::AcceptedWithFlags, + anti_cheat_flags: vec![AntiCheatFlag::TriggerCountTooHigh], + }; + + assert!( + compute_leaderboard_score(&ruleset, &input, &validation, BattleResult::PlayerWin) + .is_some() + ); + } + + #[test] + fn difficulty_changes_only_ai_preset_key() { + let easy = BarkBattleRuleset::for_difficulty(DifficultyPreset::Easy); + let normal = BarkBattleRuleset::for_difficulty(DifficultyPreset::Normal); + let hard = BarkBattleRuleset::for_difficulty(DifficultyPreset::Hard); + + assert_eq!(easy.thresholds_signature(), normal.thresholds_signature()); + assert_eq!(normal.thresholds_signature(), hard.thresholds_signature()); + assert_eq!(easy.ai_preset_key, "bark-battle-ai-easy"); + assert_eq!(normal.ai_preset_key, "bark-battle-ai-normal"); + assert_eq!(hard.ai_preset_key, "bark-battle-ai-hard"); + } +} diff --git a/server-rs/crates/shared-contracts/src/bark_battle.rs b/server-rs/crates/shared-contracts/src/bark_battle.rs new file mode 100644 index 00000000..addbd723 --- /dev/null +++ b/server-rs/crates/shared-contracts/src/bark_battle.rs @@ -0,0 +1,497 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum BarkBattleDifficultyPreset { + Easy, + Normal, + Hard, +} + +impl Default for BarkBattleDifficultyPreset { + fn default() -> Self { + Self::Normal + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum BarkBattleServerResult { + PlayerWin, + OpponentWin, + Draw, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum BarkBattleFinishStatus { + Accepted, + AcceptedWithFlags, + Rejected, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattleConfigEditorPayload { + pub title: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + pub theme_preset: String, + pub player_dog_skin_preset: String, + pub opponent_dog_skin_preset: String, + #[serde(default)] + pub difficulty_preset: BarkBattleDifficultyPreset, + pub leaderboard_enabled: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattleDraftCreateRequest { + pub title: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + pub theme_preset: String, + pub player_dog_skin_preset: String, + pub opponent_dog_skin_preset: String, + #[serde(default)] + pub difficulty_preset: BarkBattleDifficultyPreset, + pub leaderboard_enabled: bool, +} + +impl From for BarkBattleConfigEditorPayload { + fn from(value: BarkBattleDraftCreateRequest) -> Self { + Self { + title: value.title, + description: value.description, + theme_preset: value.theme_preset, + player_dog_skin_preset: value.player_dog_skin_preset, + opponent_dog_skin_preset: value.opponent_dog_skin_preset, + difficulty_preset: value.difficulty_preset, + leaderboard_enabled: value.leaderboard_enabled, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattleWorkPublishRequest { + pub draft_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub work_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub published_snapshot: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattleDraftConfig { + pub draft_id: String, + pub title: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + pub theme_preset: String, + pub player_dog_skin_preset: String, + pub opponent_dog_skin_preset: String, + #[serde(default)] + pub difficulty_preset: BarkBattleDifficultyPreset, + pub leaderboard_enabled: bool, + pub updated_at: String, +} + +impl Default for BarkBattleDraftConfig { + fn default() -> Self { + Self { + draft_id: String::new(), + title: String::new(), + description: None, + theme_preset: String::new(), + player_dog_skin_preset: String::new(), + opponent_dog_skin_preset: String::new(), + difficulty_preset: BarkBattleDifficultyPreset::Normal, + leaderboard_enabled: true, + updated_at: String::new(), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattlePublishedConfig { + pub work_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub draft_id: Option, + pub config_version: u32, + pub ruleset_version: String, + pub play_type_id: String, + pub title: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + pub theme_preset: String, + pub player_dog_skin_preset: String, + pub opponent_dog_skin_preset: String, + pub difficulty_preset: BarkBattleDifficultyPreset, + pub leaderboard_enabled: bool, + pub updated_at: String, + pub published_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattleRuntimeConfig { + pub work_id: String, + pub config_version: u32, + pub ruleset_version: String, + pub play_type_id: String, + pub duration_ms: u64, + pub energy_min: f32, + pub energy_max: f32, + pub draw_threshold: f32, + pub min_bark_gap_ms: u64, + pub difficulty_preset: BarkBattleDifficultyPreset, + pub theme_preset: String, + pub player_dog_skin_preset: String, + pub opponent_dog_skin_preset: String, + pub leaderboard_enabled: bool, + pub updated_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattleRunStartRequest { + pub work_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub config_version: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub source_route: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub client_runtime_version: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattleRunStartResponse { + pub run_id: String, + pub run_token: String, + pub work_id: String, + pub config_version: u32, + pub ruleset_version: String, + pub difficulty_preset: BarkBattleDifficultyPreset, + pub runtime_config: BarkBattleRuntimeConfig, + pub server_started_at: String, + pub expires_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattleDerivedMetrics { + pub trigger_count: u32, + pub max_volume: f32, + pub average_volume: f32, + pub final_energy: f32, + pub combo_max: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattleRunFinishRequest { + pub work_id: String, + pub run_id: String, + pub run_token: String, + pub config_version: u32, + pub ruleset_version: String, + pub difficulty_preset: BarkBattleDifficultyPreset, + pub client_started_at: String, + pub client_finished_at: String, + pub duration_ms: u64, + pub derived_metrics: BarkBattleDerivedMetrics, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub client_result: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub sample_digest: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub client_runtime_version: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattleScoreSummary { + pub duration_ms: u64, + pub trigger_count: u32, + pub max_volume: f32, + pub average_volume: f32, + pub final_energy: f32, + pub combo_max: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattleRunFinishResponse { + pub status: BarkBattleFinishStatus, + pub run_id: String, + pub work_id: String, + pub config_version: u32, + pub ruleset_version: String, + pub difficulty_preset: BarkBattleDifficultyPreset, + pub server_result: BarkBattleServerResult, + pub score_summary: BarkBattleScoreSummary, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub leaderboard_score: Option, + #[serde(default)] + pub anti_cheat_flags: Vec, + pub updated_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattleLeaderboardEntry { + pub rank: u32, + pub run_id: String, + pub work_id: String, + pub config_version: u32, + pub ruleset_version: String, + pub difficulty_preset: BarkBattleDifficultyPreset, + pub display_name: String, + pub server_result: BarkBattleServerResult, + pub score_summary: BarkBattleScoreSummary, + pub leaderboard_score: u64, + pub updated_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattleLeaderboardResponse { + pub work_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub config_version: Option, + pub ruleset_version: String, + pub difficulty_preset: BarkBattleDifficultyPreset, + #[serde(default)] + pub entries: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub viewer_best: Option, + pub updated_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattlePersonalHistoryItem { + pub run_id: String, + pub work_id: String, + pub config_version: u32, + pub ruleset_version: String, + pub difficulty_preset: BarkBattleDifficultyPreset, + pub server_result: BarkBattleServerResult, + pub score_summary: BarkBattleScoreSummary, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub leaderboard_score: Option, + #[serde(default)] + pub anti_cheat_flags: Vec, + pub updated_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattlePersonalBestSummary { + pub work_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub config_version: Option, + pub ruleset_version: String, + pub difficulty_preset: BarkBattleDifficultyPreset, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub best_leaderboard_score: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub best_final_energy: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub best_trigger_count: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub best_max_volume: Option, + pub win_count: u64, + pub draw_count: u64, + pub loss_count: u64, + pub finish_count: u64, + pub updated_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattlePersonalHistoryResponse { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub work_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub difficulty_preset: Option, + #[serde(default)] + pub items: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub best_summary: Option, + pub updated_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattleWorkStats { + pub work_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub config_version: Option, + pub ruleset_version: String, + pub difficulty_preset: BarkBattleDifficultyPreset, + pub play_start_count: u64, + pub finish_count: u64, + pub win_count: u64, + pub draw_count: u64, + pub loss_count: u64, + pub flagged_count: u64, + pub leaderboard_entry_count: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub best_leaderboard_score: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub best_final_energy: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub average_final_energy: Option, + pub updated_at: String, +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn draft_config_defaults_to_normal_difficulty() { + let config = BarkBattleDraftConfig::default(); + + assert_eq!(config.difficulty_preset, BarkBattleDifficultyPreset::Normal); + } + + #[test] + fn run_requests_carry_config_and_ruleset_identity() { + let start = BarkBattleRunStartRequest { + work_id: "work-1".to_string(), + config_version: Some(3), + source_route: Some("gallery".to_string()), + client_runtime_version: Some("runtime-v1".to_string()), + }; + let start_payload = serde_json::to_value(start).expect("start request should serialize"); + assert_eq!(start_payload["workId"], json!("work-1")); + assert_eq!(start_payload["configVersion"], json!(3)); + assert_eq!(start_payload["sourceRoute"], json!("gallery")); + + let finish = BarkBattleRunFinishRequest { + work_id: "work-1".to_string(), + run_id: "run-1".to_string(), + run_token: "token-1".to_string(), + config_version: 3, + ruleset_version: "bark-battle-ruleset-v1".to_string(), + difficulty_preset: BarkBattleDifficultyPreset::Hard, + client_started_at: "2026-05-13T11:00:00Z".to_string(), + client_finished_at: "2026-05-13T11:00:30Z".to_string(), + duration_ms: 30_000, + derived_metrics: BarkBattleDerivedMetrics { + trigger_count: 12, + max_volume: 0.95, + average_volume: 0.62, + final_energy: 88.5, + combo_max: 7, + }, + client_result: Some(BarkBattleServerResult::PlayerWin), + sample_digest: Some("digest-1".to_string()), + client_runtime_version: None, + }; + let finish_payload = serde_json::to_value(finish).expect("finish request should serialize"); + assert_eq!(finish_payload["configVersion"], json!(3)); + assert_eq!( + finish_payload["rulesetVersion"], + json!("bark-battle-ruleset-v1") + ); + assert_eq!(finish_payload["difficultyPreset"], json!("hard")); + } + + #[test] + fn optional_fields_are_omitted_when_absent() { + let draft = BarkBattleDraftConfig::default(); + let payload = serde_json::to_value(draft).expect("draft should serialize"); + assert!(!payload.as_object().unwrap().contains_key("description")); + + let response = BarkBattlePersonalHistoryResponse { + work_id: None, + difficulty_preset: None, + items: Vec::new(), + best_summary: None, + updated_at: "2026-05-13T11:00:00Z".to_string(), + }; + let payload = serde_json::to_value(response).expect("history response should serialize"); + assert!(!payload.as_object().unwrap().contains_key("workId")); + assert!( + !payload + .as_object() + .unwrap() + .contains_key("difficultyPreset") + ); + assert!(!payload.as_object().unwrap().contains_key("bestSummary")); + } + + #[test] + fn finish_response_serializes_player_win_and_accepted() { + let response = BarkBattleRunFinishResponse { + status: BarkBattleFinishStatus::Accepted, + run_id: "run-1".to_string(), + work_id: "work-1".to_string(), + config_version: 3, + ruleset_version: "bark-battle-ruleset-v1".to_string(), + difficulty_preset: BarkBattleDifficultyPreset::Normal, + server_result: BarkBattleServerResult::PlayerWin, + score_summary: BarkBattleScoreSummary { + duration_ms: 30_000, + trigger_count: 12, + max_volume: 0.95, + average_volume: 0.62, + final_energy: 88.5, + combo_max: 7, + }, + leaderboard_score: Some(98_765), + anti_cheat_flags: Vec::new(), + updated_at: "2026-05-13T11:00:00Z".to_string(), + }; + + let payload = serde_json::to_value(response).expect("finish response should serialize"); + + assert_eq!(payload["runId"], json!("run-1")); + assert_eq!(payload["status"], json!("accepted")); + assert_eq!(payload["serverResult"], json!("player_win")); + assert_eq!(payload["leaderboardScore"], json!(98_765)); + assert_eq!(payload["scoreSummary"]["finalEnergy"], json!(88.5)); + } + + #[test] + fn work_stats_fields_are_constructible() { + let stats = BarkBattleWorkStats { + work_id: "work-1".to_string(), + config_version: Some(3), + ruleset_version: "bark-battle-ruleset-v1".to_string(), + difficulty_preset: BarkBattleDifficultyPreset::Normal, + play_start_count: 10, + finish_count: 9, + win_count: 5, + draw_count: 2, + loss_count: 2, + flagged_count: 1, + leaderboard_entry_count: 4, + best_leaderboard_score: Some(98_765), + best_final_energy: Some(97.5), + average_final_energy: Some(73.25), + updated_at: "2026-05-13T11:00:00Z".to_string(), + }; + + assert_eq!(stats.work_id, "work-1"); + assert_eq!(stats.play_start_count, 10); + assert_eq!(stats.finish_count, 9); + assert_eq!(stats.win_count, 5); + assert_eq!(stats.draw_count, 2); + assert_eq!(stats.loss_count, 2); + assert_eq!(stats.flagged_count, 1); + assert_eq!(stats.leaderboard_entry_count, 4); + assert_eq!(stats.best_leaderboard_score, Some(98_765)); + assert_eq!(stats.best_final_energy, Some(97.5)); + assert_eq!(stats.average_final_energy, Some(73.25)); + assert_eq!(stats.updated_at, "2026-05-13T11:00:00Z"); + } +} diff --git a/server-rs/crates/shared-contracts/src/lib.rs b/server-rs/crates/shared-contracts/src/lib.rs index 35878994..19c713fd 100644 --- a/server-rs/crates/shared-contracts/src/lib.rs +++ b/server-rs/crates/shared-contracts/src/lib.rs @@ -4,6 +4,7 @@ pub mod api; #[cfg(feature = "oss-contracts")] pub mod assets; pub mod auth; +pub mod bark_battle; pub mod big_fish; pub mod big_fish_works; pub mod creation_agent_document_input; diff --git a/server-rs/crates/spacetime-client/src/bark_battle.rs b/server-rs/crates/spacetime-client/src/bark_battle.rs new file mode 100644 index 00000000..18985b15 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/bark_battle.rs @@ -0,0 +1,138 @@ +use super::*; + +pub type BarkBattleDraftCreateRecordInput = BarkBattleDraftCreateInput; +pub type BarkBattleDraftConfigUpsertRecordInput = BarkBattleDraftConfigUpsertInput; +pub type BarkBattleWorkPublishRecordInput = BarkBattleWorkPublishInput; +pub type BarkBattleRunStartRecordInput = BarkBattleRunStartInput; +pub type BarkBattleRunFinishRecordInput = BarkBattleRunFinishInput; + +impl SpacetimeClient { + pub async fn create_bark_battle_draft( + &self, + input: BarkBattleDraftCreateRecordInput, + ) -> Result { + self.call_after_connect(move |connection, sender| { + connection + .procedures() + .create_bark_battle_draft_then(input, move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_bark_battle_draft_config_procedure_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn update_bark_battle_draft_config( + &self, + input: BarkBattleDraftConfigUpsertRecordInput, + ) -> Result { + self.call_after_connect(move |connection, sender| { + connection + .procedures() + .update_bark_battle_draft_config_then(input, move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_bark_battle_draft_config_procedure_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn publish_bark_battle_work( + &self, + input: BarkBattleWorkPublishRecordInput, + ) -> Result { + self.call_after_connect(move |connection, sender| { + connection + .procedures() + .publish_bark_battle_work_then(input, move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_bark_battle_runtime_config_procedure_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn get_bark_battle_runtime_config( + &self, + work_id: String, + owner_user_id: Option, + ) -> Result { + let input = BarkBattleRuntimeConfigGetInput { + work_id, + owner_user_id, + }; + self.call_after_connect(move |connection, sender| { + connection + .procedures() + .get_bark_battle_runtime_config_then(input, move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_bark_battle_runtime_config_procedure_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn start_bark_battle_run( + &self, + input: BarkBattleRunStartRecordInput, + ) -> Result { + self.call_after_connect(move |connection, sender| { + connection + .procedures() + .start_bark_battle_run_then(input, move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_bark_battle_run_procedure_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn finish_bark_battle_run( + &self, + input: BarkBattleRunFinishRecordInput, + ) -> Result { + self.call_after_connect(move |connection, sender| { + connection + .procedures() + .finish_bark_battle_run_then(input, move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_bark_battle_run_procedure_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn get_bark_battle_run( + &self, + run_id: String, + owner_user_id: String, + ) -> Result { + let input = BarkBattleRunGetInput { + run_id, + owner_user_id, + }; + self.call_after_connect(move |connection, sender| { + connection + .procedures() + .get_bark_battle_run_then(input, move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_bark_battle_run_procedure_result); + send_once(&sender, mapped); + }); + }) + .await + } +} diff --git a/server-rs/crates/spacetime-client/src/lib.rs b/server-rs/crates/spacetime-client/src/lib.rs index c9844b95..4d8c7f2d 100644 --- a/server-rs/crates/spacetime-client/src/lib.rs +++ b/server-rs/crates/spacetime-client/src/lib.rs @@ -6,21 +6,22 @@ mod mapper; use mapper::*; pub use mapper::{ AiResultReferenceRecord, AiTaskMutationRecord, AiTaskRecord, AiTaskStageRecord, - AiTextChunkRecord, BattleStateRecord, BigFishAgentMessageRecord, BigFishAnchorItemRecord, - BigFishAnchorPackRecord, BigFishAssetCoverageRecord, BigFishAssetGenerateRecordInput, - BigFishAssetSlotRecord, BigFishBackgroundBlueprintRecord, BigFishDraftCompileRecordInput, - BigFishGameDraftRecord, BigFishInputSubmitRecordInput, BigFishLevelBlueprintRecord, - BigFishLikeReportRecordInput, BigFishMessageFinalizeRecordInput, + AiTextChunkRecord, BarkBattleDraftConfigRecord, BarkBattleRunRecord, + BarkBattleRuntimeConfigRecord, BattleStateRecord, BigFishAgentMessageRecord, + BigFishAnchorItemRecord, BigFishAnchorPackRecord, BigFishAssetCoverageRecord, + BigFishAssetGenerateRecordInput, BigFishAssetSlotRecord, BigFishBackgroundBlueprintRecord, + BigFishDraftCompileRecordInput, BigFishGameDraftRecord, BigFishInputSubmitRecordInput, + BigFishLevelBlueprintRecord, BigFishLikeReportRecordInput, BigFishMessageFinalizeRecordInput, BigFishMessageSubmitRecordInput, BigFishPlayReportRecordInput, BigFishRunStartRecordInput, BigFishRuntimeEntityRecord, BigFishRuntimeParamsRecord, BigFishRuntimeRunRecord, BigFishSessionCreateRecordInput, BigFishSessionRecord, BigFishVector2Record, BigFishWorkRemixRecordInput, BigFishWorkSummaryRecord, CreationEntryConfigRecord, - CustomWorldAgentActionExecuteRecord, - CustomWorldAgentActionExecuteRecordInput, CustomWorldAgentCheckpointRecord, - CustomWorldAgentMessageFinalizeRecordInput, CustomWorldAgentMessageRecord, - CustomWorldAgentMessageSubmitRecordInput, CustomWorldAgentOperationProgressRecordInput, - CustomWorldAgentOperationRecord, CustomWorldAgentSessionCreateRecordInput, - CustomWorldAgentSessionRecord, CustomWorldCheckpointRecord, CustomWorldDraftCardDetailRecord, + CustomWorldAgentActionExecuteRecord, CustomWorldAgentActionExecuteRecordInput, + CustomWorldAgentCheckpointRecord, CustomWorldAgentMessageFinalizeRecordInput, + CustomWorldAgentMessageRecord, CustomWorldAgentMessageSubmitRecordInput, + CustomWorldAgentOperationProgressRecordInput, CustomWorldAgentOperationRecord, + CustomWorldAgentSessionCreateRecordInput, CustomWorldAgentSessionRecord, + CustomWorldCheckpointRecord, CustomWorldDraftCardDetailRecord, CustomWorldDraftCardDetailSectionRecord, CustomWorldDraftCardRecord, CustomWorldGalleryEntryRecord, CustomWorldLibraryEntryRecord, CustomWorldLibraryMutationRecord, CustomWorldProfileLikeReportRecordInput, CustomWorldProfilePlayReportRecordInput, @@ -42,15 +43,14 @@ pub use mapper::{ PuzzleAgentSessionRecord, PuzzleAgentSuggestedActionRecord, PuzzleAnchorItemRecord, PuzzleAnchorPackRecord, PuzzleAudioAssetRecord, PuzzleBoardRecord, PuzzleCellPositionRecord, PuzzleCreatorIntentRecord, PuzzleDraftLevelRecord, PuzzleFormDraftRecord, - PuzzleFormDraftSaveRecordInput, - PuzzleGeneratedImageCandidateRecord, PuzzleGeneratedImagesSaveRecordInput, - PuzzleLeaderboardEntryRecord, PuzzleLeaderboardSubmitRecordInput, PuzzleMergedGroupRecord, - PuzzlePieceStateRecord, PuzzlePublishRecordInput, PuzzleRecommendedNextWorkRecord, - PuzzleResultDraftRecord, PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, - PuzzleResultPreviewRecord, PuzzleRunDragRecordInput, PuzzleRunNextLevelRecordInput, - PuzzleRunPauseRecordInput, PuzzleRunPropRecordInput, PuzzleRunRecord, - PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, PuzzleRuntimeLevelRecord, - PuzzleSelectCoverImageRecordInput, PuzzleWorkLikeReportRecordInput, + PuzzleFormDraftSaveRecordInput, PuzzleGeneratedImageCandidateRecord, + PuzzleGeneratedImagesSaveRecordInput, PuzzleLeaderboardEntryRecord, + PuzzleLeaderboardSubmitRecordInput, PuzzleMergedGroupRecord, PuzzlePieceStateRecord, + PuzzlePublishRecordInput, PuzzleRecommendedNextWorkRecord, PuzzleResultDraftRecord, + PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, PuzzleResultPreviewRecord, + PuzzleRunDragRecordInput, PuzzleRunNextLevelRecordInput, PuzzleRunPauseRecordInput, + PuzzleRunPropRecordInput, PuzzleRunRecord, PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, + PuzzleRuntimeLevelRecord, PuzzleSelectCoverImageRecordInput, PuzzleWorkLikeReportRecordInput, PuzzleWorkPointIncentiveClaimRecordInput, PuzzleWorkProfileRecord, PuzzleWorkRemixRecordInput, PuzzleWorkUpsertRecordInput, ResolveCombatActionRecord, ResolveNpcBattleInteractionInput, SquareHoleAgentMessageFinalizeRecordInput, SquareHoleAgentMessageRecord, @@ -74,6 +74,12 @@ pub use mapper::{ pub mod ai; pub mod assets; pub mod auth; +pub mod bark_battle; +pub use bark_battle::{ + BarkBattleDraftConfigUpsertRecordInput, BarkBattleDraftCreateRecordInput, + BarkBattleRunFinishRecordInput, BarkBattleRunStartRecordInput, + BarkBattleWorkPublishRecordInput, +}; pub mod big_fish; pub mod combat; pub mod custom_world; diff --git a/server-rs/crates/spacetime-client/src/mapper.rs b/server-rs/crates/spacetime-client/src/mapper.rs index 5e5ee05d..b963cc05 100644 --- a/server-rs/crates/spacetime-client/src/mapper.rs +++ b/server-rs/crates/spacetime-client/src/mapper.rs @@ -720,6 +720,42 @@ pub(crate) fn map_asset_history_list_result( .collect()) } +pub type BarkBattleDraftConfigRecord = serde_json::Value; +pub type BarkBattleRuntimeConfigRecord = serde_json::Value; +pub type BarkBattleRunRecord = serde_json::Value; + +pub(crate) fn map_bark_battle_draft_config_procedure_result( + result: BarkBattleProcedureResult, +) -> Result { + parse_bark_battle_row_json(result, "Bark Battle draft config") +} + +pub(crate) fn map_bark_battle_runtime_config_procedure_result( + result: BarkBattleProcedureResult, +) -> Result { + parse_bark_battle_row_json(result, "Bark Battle runtime config") +} + +pub(crate) fn map_bark_battle_run_procedure_result( + result: BarkBattleProcedureResult, +) -> Result { + parse_bark_battle_row_json(result, "Bark Battle run") +} + +fn parse_bark_battle_row_json( + result: BarkBattleProcedureResult, + label: &'static str, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + let row_json = result + .row_json + .ok_or_else(|| SpacetimeClientError::missing_snapshot(label))?; + serde_json::from_str(&row_json) + .map_err(|error| SpacetimeClientError::Runtime(format!("{label} JSON 解析失败: {error}"))) +} + pub type CreationEntryConfigRecord = shared_contracts::creation_entry_config::CreationEntryConfigResponse; diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_draft_config_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_draft_config_row_type.rs new file mode 100644 index 00000000..37494c81 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_draft_config_row_type.rs @@ -0,0 +1,86 @@ +// 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 BarkBattleDraftConfigRow { + pub draft_id: String, + pub owner_user_id: String, + pub work_id: String, + pub config_version: u64, + pub ruleset_version: String, + pub difficulty_preset: String, + pub leaderboard_enabled: bool, + pub config_json: String, + pub editor_state_json: String, + pub created_at: __sdk::Timestamp, + pub updated_at: __sdk::Timestamp, +} + +impl __sdk::InModule for BarkBattleDraftConfigRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `BarkBattleDraftConfigRow`. +/// +/// Provides typed access to columns for query building. +pub struct BarkBattleDraftConfigRowCols { + pub draft_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub work_id: __sdk::__query_builder::Col, + pub config_version: __sdk::__query_builder::Col, + pub ruleset_version: __sdk::__query_builder::Col, + pub difficulty_preset: __sdk::__query_builder::Col, + pub leaderboard_enabled: __sdk::__query_builder::Col, + pub config_json: __sdk::__query_builder::Col, + pub editor_state_json: __sdk::__query_builder::Col, + pub created_at: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for BarkBattleDraftConfigRow { + type Cols = BarkBattleDraftConfigRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + BarkBattleDraftConfigRowCols { + draft_id: __sdk::__query_builder::Col::new(table_name, "draft_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + work_id: __sdk::__query_builder::Col::new(table_name, "work_id"), + config_version: __sdk::__query_builder::Col::new(table_name, "config_version"), + ruleset_version: __sdk::__query_builder::Col::new(table_name, "ruleset_version"), + difficulty_preset: __sdk::__query_builder::Col::new(table_name, "difficulty_preset"), + leaderboard_enabled: __sdk::__query_builder::Col::new( + table_name, + "leaderboard_enabled", + ), + config_json: __sdk::__query_builder::Col::new(table_name, "config_json"), + editor_state_json: __sdk::__query_builder::Col::new(table_name, "editor_state_json"), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + } + } +} + +/// Indexed column accessor struct for the table `BarkBattleDraftConfigRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct BarkBattleDraftConfigRowIxCols { + pub draft_id: __sdk::__query_builder::IxCol, + pub owner_user_id: __sdk::__query_builder::IxCol, + pub work_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for BarkBattleDraftConfigRow { + type IxCols = BarkBattleDraftConfigRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + BarkBattleDraftConfigRowIxCols { + draft_id: __sdk::__query_builder::IxCol::new(table_name, "draft_id"), + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + work_id: __sdk::__query_builder::IxCol::new(table_name, "work_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for BarkBattleDraftConfigRow {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_draft_config_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_draft_config_table.rs new file mode 100644 index 00000000..b9e28c14 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_draft_config_table.rs @@ -0,0 +1,162 @@ +// 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::bark_battle_draft_config_row_type::BarkBattleDraftConfigRow; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `bark_battle_draft_config`. +/// +/// Obtain a handle from the [`BarkBattleDraftConfigTableAccess::bark_battle_draft_config`] method on [`super::RemoteTables`], +/// like `ctx.db.bark_battle_draft_config()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.bark_battle_draft_config().on_insert(...)`. +pub struct BarkBattleDraftConfigTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `bark_battle_draft_config`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait BarkBattleDraftConfigTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`BarkBattleDraftConfigTableHandle`], which mediates access to the table `bark_battle_draft_config`. + fn bark_battle_draft_config(&self) -> BarkBattleDraftConfigTableHandle<'_>; +} + +impl BarkBattleDraftConfigTableAccess for super::RemoteTables { + fn bark_battle_draft_config(&self) -> BarkBattleDraftConfigTableHandle<'_> { + BarkBattleDraftConfigTableHandle { + imp: self + .imp + .get_table::("bark_battle_draft_config"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct BarkBattleDraftConfigInsertCallbackId(__sdk::CallbackId); +pub struct BarkBattleDraftConfigDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for BarkBattleDraftConfigTableHandle<'ctx> { + type Row = BarkBattleDraftConfigRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = BarkBattleDraftConfigInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BarkBattleDraftConfigInsertCallbackId { + BarkBattleDraftConfigInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: BarkBattleDraftConfigInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = BarkBattleDraftConfigDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BarkBattleDraftConfigDeleteCallbackId { + BarkBattleDraftConfigDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: BarkBattleDraftConfigDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct BarkBattleDraftConfigUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for BarkBattleDraftConfigTableHandle<'ctx> { + type UpdateCallbackId = BarkBattleDraftConfigUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> BarkBattleDraftConfigUpdateCallbackId { + BarkBattleDraftConfigUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: BarkBattleDraftConfigUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `draft_id` unique index on the table `bark_battle_draft_config`, +/// which allows point queries on the field of the same name +/// via the [`BarkBattleDraftConfigDraftIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.bark_battle_draft_config().draft_id().find(...)`. +pub struct BarkBattleDraftConfigDraftIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> BarkBattleDraftConfigTableHandle<'ctx> { + /// Get a handle on the `draft_id` unique index on the table `bark_battle_draft_config`. + pub fn draft_id(&self) -> BarkBattleDraftConfigDraftIdUnique<'ctx> { + BarkBattleDraftConfigDraftIdUnique { + imp: self.imp.get_unique_constraint::("draft_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> BarkBattleDraftConfigDraftIdUnique<'ctx> { + /// Find the subscribed row whose `draft_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 { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = + client_cache.get_or_make_table::("bark_battle_draft_config"); + _table.add_unique_constraint::("draft_id", |row| &row.draft_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `BarkBattleDraftConfigRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait bark_battle_draft_configQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `BarkBattleDraftConfigRow`. + fn bark_battle_draft_config(&self) -> __sdk::__query_builder::Table; +} + +impl bark_battle_draft_configQueryTableAccess for __sdk::QueryTableAccessor { + fn bark_battle_draft_config(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("bark_battle_draft_config") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_draft_config_upsert_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_draft_config_upsert_input_type.rs new file mode 100644 index 00000000..ee07d0c0 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_draft_config_upsert_input_type.rs @@ -0,0 +1,23 @@ +// 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 BarkBattleDraftConfigUpsertInput { + pub draft_id: String, + pub owner_user_id: String, + pub work_id: String, + pub config_version: u64, + pub ruleset_version: String, + pub difficulty_preset: String, + pub leaderboard_enabled: bool, + pub config_json: String, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for BarkBattleDraftConfigUpsertInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_draft_create_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_draft_create_input_type.rs new file mode 100644 index 00000000..7e165bfb --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_draft_create_input_type.rs @@ -0,0 +1,26 @@ +// 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 BarkBattleDraftCreateInput { + pub draft_id: String, + pub owner_user_id: String, + pub work_id: String, + pub title: Option, + pub description: Option, + pub theme_preset: String, + pub player_dog_skin_preset: String, + pub opponent_dog_skin_preset: String, + pub difficulty_preset: Option, + pub leaderboard_enabled: Option, + pub editor_state_json: Option, + pub created_at_micros: i64, +} + +impl __sdk::InModule for BarkBattleDraftCreateInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_leaderboard_entry_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_leaderboard_entry_row_type.rs new file mode 100644 index 00000000..c23b8e6d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_leaderboard_entry_row_type.rs @@ -0,0 +1,94 @@ +// 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 BarkBattleLeaderboardEntryRow { + pub leaderboard_entry_id: String, + pub work_id: String, + pub owner_user_id: String, + pub run_id: String, + pub score_id: String, + pub leaderboard_score: u64, + pub final_energy: f32, + pub trigger_count: u64, + pub max_volume: f32, + pub duration_closeness_ms: u64, + pub finished_at_micros: i64, + pub created_at: __sdk::Timestamp, + pub updated_at: __sdk::Timestamp, +} + +impl __sdk::InModule for BarkBattleLeaderboardEntryRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `BarkBattleLeaderboardEntryRow`. +/// +/// Provides typed access to columns for query building. +pub struct BarkBattleLeaderboardEntryRowCols { + pub leaderboard_entry_id: __sdk::__query_builder::Col, + pub work_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub run_id: __sdk::__query_builder::Col, + pub score_id: __sdk::__query_builder::Col, + pub leaderboard_score: __sdk::__query_builder::Col, + pub final_energy: __sdk::__query_builder::Col, + pub trigger_count: __sdk::__query_builder::Col, + pub max_volume: __sdk::__query_builder::Col, + pub duration_closeness_ms: __sdk::__query_builder::Col, + pub finished_at_micros: __sdk::__query_builder::Col, + pub created_at: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for BarkBattleLeaderboardEntryRow { + type Cols = BarkBattleLeaderboardEntryRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + BarkBattleLeaderboardEntryRowCols { + leaderboard_entry_id: __sdk::__query_builder::Col::new( + table_name, + "leaderboard_entry_id", + ), + work_id: __sdk::__query_builder::Col::new(table_name, "work_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + run_id: __sdk::__query_builder::Col::new(table_name, "run_id"), + score_id: __sdk::__query_builder::Col::new(table_name, "score_id"), + leaderboard_score: __sdk::__query_builder::Col::new(table_name, "leaderboard_score"), + final_energy: __sdk::__query_builder::Col::new(table_name, "final_energy"), + trigger_count: __sdk::__query_builder::Col::new(table_name, "trigger_count"), + max_volume: __sdk::__query_builder::Col::new(table_name, "max_volume"), + duration_closeness_ms: __sdk::__query_builder::Col::new( + table_name, + "duration_closeness_ms", + ), + finished_at_micros: __sdk::__query_builder::Col::new(table_name, "finished_at_micros"), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + } + } +} + +/// Indexed column accessor struct for the table `BarkBattleLeaderboardEntryRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct BarkBattleLeaderboardEntryRowIxCols { + pub leaderboard_entry_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for BarkBattleLeaderboardEntryRow { + type IxCols = BarkBattleLeaderboardEntryRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + BarkBattleLeaderboardEntryRowIxCols { + leaderboard_entry_id: __sdk::__query_builder::IxCol::new( + table_name, + "leaderboard_entry_id", + ), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for BarkBattleLeaderboardEntryRow {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_leaderboard_entry_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_leaderboard_entry_table.rs new file mode 100644 index 00000000..a4ed2f3d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_leaderboard_entry_table.rs @@ -0,0 +1,171 @@ +// 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::bark_battle_leaderboard_entry_row_type::BarkBattleLeaderboardEntryRow; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `bark_battle_leaderboard_entry`. +/// +/// Obtain a handle from the [`BarkBattleLeaderboardEntryTableAccess::bark_battle_leaderboard_entry`] method on [`super::RemoteTables`], +/// like `ctx.db.bark_battle_leaderboard_entry()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.bark_battle_leaderboard_entry().on_insert(...)`. +pub struct BarkBattleLeaderboardEntryTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `bark_battle_leaderboard_entry`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait BarkBattleLeaderboardEntryTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`BarkBattleLeaderboardEntryTableHandle`], which mediates access to the table `bark_battle_leaderboard_entry`. + fn bark_battle_leaderboard_entry(&self) -> BarkBattleLeaderboardEntryTableHandle<'_>; +} + +impl BarkBattleLeaderboardEntryTableAccess for super::RemoteTables { + fn bark_battle_leaderboard_entry(&self) -> BarkBattleLeaderboardEntryTableHandle<'_> { + BarkBattleLeaderboardEntryTableHandle { + imp: self + .imp + .get_table::("bark_battle_leaderboard_entry"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct BarkBattleLeaderboardEntryInsertCallbackId(__sdk::CallbackId); +pub struct BarkBattleLeaderboardEntryDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for BarkBattleLeaderboardEntryTableHandle<'ctx> { + type Row = BarkBattleLeaderboardEntryRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = BarkBattleLeaderboardEntryInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BarkBattleLeaderboardEntryInsertCallbackId { + BarkBattleLeaderboardEntryInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: BarkBattleLeaderboardEntryInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = BarkBattleLeaderboardEntryDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BarkBattleLeaderboardEntryDeleteCallbackId { + BarkBattleLeaderboardEntryDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: BarkBattleLeaderboardEntryDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct BarkBattleLeaderboardEntryUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for BarkBattleLeaderboardEntryTableHandle<'ctx> { + type UpdateCallbackId = BarkBattleLeaderboardEntryUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> BarkBattleLeaderboardEntryUpdateCallbackId { + BarkBattleLeaderboardEntryUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: BarkBattleLeaderboardEntryUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `leaderboard_entry_id` unique index on the table `bark_battle_leaderboard_entry`, +/// which allows point queries on the field of the same name +/// via the [`BarkBattleLeaderboardEntryLeaderboardEntryIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.bark_battle_leaderboard_entry().leaderboard_entry_id().find(...)`. +pub struct BarkBattleLeaderboardEntryLeaderboardEntryIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> BarkBattleLeaderboardEntryTableHandle<'ctx> { + /// Get a handle on the `leaderboard_entry_id` unique index on the table `bark_battle_leaderboard_entry`. + pub fn leaderboard_entry_id(&self) -> BarkBattleLeaderboardEntryLeaderboardEntryIdUnique<'ctx> { + BarkBattleLeaderboardEntryLeaderboardEntryIdUnique { + imp: self + .imp + .get_unique_constraint::("leaderboard_entry_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> BarkBattleLeaderboardEntryLeaderboardEntryIdUnique<'ctx> { + /// Find the subscribed row whose `leaderboard_entry_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 { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache + .get_or_make_table::("bark_battle_leaderboard_entry"); + _table.add_unique_constraint::("leaderboard_entry_id", |row| &row.leaderboard_entry_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse( + "TableUpdate", + "TableUpdate", + ) + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `BarkBattleLeaderboardEntryRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait bark_battle_leaderboard_entryQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `BarkBattleLeaderboardEntryRow`. + fn bark_battle_leaderboard_entry( + &self, + ) -> __sdk::__query_builder::Table; +} + +impl bark_battle_leaderboard_entryQueryTableAccess for __sdk::QueryTableAccessor { + fn bark_battle_leaderboard_entry( + &self, + ) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("bark_battle_leaderboard_entry") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_personal_best_projection_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_personal_best_projection_row_type.rs new file mode 100644 index 00000000..e7a88283 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_personal_best_projection_row_type.rs @@ -0,0 +1,107 @@ +// 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 BarkBattlePersonalBestProjectionRow { + pub personal_best_id: String, + pub owner_user_id: String, + pub work_id: String, + pub run_id: String, + pub score_id: String, + pub leaderboard_entry_id: Option, + pub leaderboard_score: Option, + pub final_energy: f32, + pub trigger_count: u64, + pub max_volume: f32, + pub duration_closeness_ms: u64, + pub server_result: String, + pub validation_status: String, + pub finished_at_micros: i64, + pub summary_json: String, + pub updated_at: __sdk::Timestamp, +} + +impl __sdk::InModule for BarkBattlePersonalBestProjectionRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `BarkBattlePersonalBestProjectionRow`. +/// +/// Provides typed access to columns for query building. +pub struct BarkBattlePersonalBestProjectionRowCols { + pub personal_best_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub work_id: __sdk::__query_builder::Col, + pub run_id: __sdk::__query_builder::Col, + pub score_id: __sdk::__query_builder::Col, + pub leaderboard_entry_id: + __sdk::__query_builder::Col>, + pub leaderboard_score: + __sdk::__query_builder::Col>, + pub final_energy: __sdk::__query_builder::Col, + pub trigger_count: __sdk::__query_builder::Col, + pub max_volume: __sdk::__query_builder::Col, + pub duration_closeness_ms: + __sdk::__query_builder::Col, + pub server_result: __sdk::__query_builder::Col, + pub validation_status: __sdk::__query_builder::Col, + pub finished_at_micros: __sdk::__query_builder::Col, + pub summary_json: __sdk::__query_builder::Col, + pub updated_at: + __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for BarkBattlePersonalBestProjectionRow { + type Cols = BarkBattlePersonalBestProjectionRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + BarkBattlePersonalBestProjectionRowCols { + personal_best_id: __sdk::__query_builder::Col::new(table_name, "personal_best_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + work_id: __sdk::__query_builder::Col::new(table_name, "work_id"), + run_id: __sdk::__query_builder::Col::new(table_name, "run_id"), + score_id: __sdk::__query_builder::Col::new(table_name, "score_id"), + leaderboard_entry_id: __sdk::__query_builder::Col::new( + table_name, + "leaderboard_entry_id", + ), + leaderboard_score: __sdk::__query_builder::Col::new(table_name, "leaderboard_score"), + final_energy: __sdk::__query_builder::Col::new(table_name, "final_energy"), + trigger_count: __sdk::__query_builder::Col::new(table_name, "trigger_count"), + max_volume: __sdk::__query_builder::Col::new(table_name, "max_volume"), + duration_closeness_ms: __sdk::__query_builder::Col::new( + table_name, + "duration_closeness_ms", + ), + server_result: __sdk::__query_builder::Col::new(table_name, "server_result"), + validation_status: __sdk::__query_builder::Col::new(table_name, "validation_status"), + finished_at_micros: __sdk::__query_builder::Col::new(table_name, "finished_at_micros"), + summary_json: __sdk::__query_builder::Col::new(table_name, "summary_json"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + } + } +} + +/// Indexed column accessor struct for the table `BarkBattlePersonalBestProjectionRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct BarkBattlePersonalBestProjectionRowIxCols { + pub personal_best_id: + __sdk::__query_builder::IxCol, + pub work_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for BarkBattlePersonalBestProjectionRow { + type IxCols = BarkBattlePersonalBestProjectionRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + BarkBattlePersonalBestProjectionRowIxCols { + personal_best_id: __sdk::__query_builder::IxCol::new(table_name, "personal_best_id"), + work_id: __sdk::__query_builder::IxCol::new(table_name, "work_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for BarkBattlePersonalBestProjectionRow {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_personal_best_projection_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_personal_best_projection_table.rs new file mode 100644 index 00000000..d5a142bf --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_personal_best_projection_table.rs @@ -0,0 +1,174 @@ +// 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::bark_battle_personal_best_projection_row_type::BarkBattlePersonalBestProjectionRow; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `bark_battle_personal_best_projection`. +/// +/// Obtain a handle from the [`BarkBattlePersonalBestProjectionTableAccess::bark_battle_personal_best_projection`] method on [`super::RemoteTables`], +/// like `ctx.db.bark_battle_personal_best_projection()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.bark_battle_personal_best_projection().on_insert(...)`. +pub struct BarkBattlePersonalBestProjectionTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `bark_battle_personal_best_projection`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait BarkBattlePersonalBestProjectionTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`BarkBattlePersonalBestProjectionTableHandle`], which mediates access to the table `bark_battle_personal_best_projection`. + fn bark_battle_personal_best_projection( + &self, + ) -> BarkBattlePersonalBestProjectionTableHandle<'_>; +} + +impl BarkBattlePersonalBestProjectionTableAccess for super::RemoteTables { + fn bark_battle_personal_best_projection( + &self, + ) -> BarkBattlePersonalBestProjectionTableHandle<'_> { + BarkBattlePersonalBestProjectionTableHandle { + imp: self.imp.get_table::( + "bark_battle_personal_best_projection", + ), + ctx: std::marker::PhantomData, + } + } +} + +pub struct BarkBattlePersonalBestProjectionInsertCallbackId(__sdk::CallbackId); +pub struct BarkBattlePersonalBestProjectionDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for BarkBattlePersonalBestProjectionTableHandle<'ctx> { + type Row = BarkBattlePersonalBestProjectionRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = BarkBattlePersonalBestProjectionInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BarkBattlePersonalBestProjectionInsertCallbackId { + BarkBattlePersonalBestProjectionInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: BarkBattlePersonalBestProjectionInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = BarkBattlePersonalBestProjectionDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BarkBattlePersonalBestProjectionDeleteCallbackId { + BarkBattlePersonalBestProjectionDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: BarkBattlePersonalBestProjectionDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct BarkBattlePersonalBestProjectionUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for BarkBattlePersonalBestProjectionTableHandle<'ctx> { + type UpdateCallbackId = BarkBattlePersonalBestProjectionUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> BarkBattlePersonalBestProjectionUpdateCallbackId { + BarkBattlePersonalBestProjectionUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: BarkBattlePersonalBestProjectionUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `personal_best_id` unique index on the table `bark_battle_personal_best_projection`, +/// which allows point queries on the field of the same name +/// via the [`BarkBattlePersonalBestProjectionPersonalBestIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.bark_battle_personal_best_projection().personal_best_id().find(...)`. +pub struct BarkBattlePersonalBestProjectionPersonalBestIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> BarkBattlePersonalBestProjectionTableHandle<'ctx> { + /// Get a handle on the `personal_best_id` unique index on the table `bark_battle_personal_best_projection`. + pub fn personal_best_id(&self) -> BarkBattlePersonalBestProjectionPersonalBestIdUnique<'ctx> { + BarkBattlePersonalBestProjectionPersonalBestIdUnique { + imp: self.imp.get_unique_constraint::("personal_best_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> BarkBattlePersonalBestProjectionPersonalBestIdUnique<'ctx> { + /// Find the subscribed row whose `personal_best_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 { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::( + "bark_battle_personal_best_projection", + ); + _table.add_unique_constraint::("personal_best_id", |row| &row.personal_best_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse( + "TableUpdate", + "TableUpdate", + ) + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `BarkBattlePersonalBestProjectionRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait bark_battle_personal_best_projectionQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `BarkBattlePersonalBestProjectionRow`. + fn bark_battle_personal_best_projection( + &self, + ) -> __sdk::__query_builder::Table; +} + +impl bark_battle_personal_best_projectionQueryTableAccess for __sdk::QueryTableAccessor { + fn bark_battle_personal_best_projection( + &self, + ) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("bark_battle_personal_best_projection") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_procedure_result_type.rs new file mode 100644 index 00000000..6fe7a3ee --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_procedure_result_type.rs @@ -0,0 +1,17 @@ +// 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 BarkBattleProcedureResult { + pub ok: bool, + pub row_json: Option, + pub error_message: Option, +} + +impl __sdk::InModule for BarkBattleProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_published_config_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_published_config_row_type.rs new file mode 100644 index 00000000..c442c6bc --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_published_config_row_type.rs @@ -0,0 +1,90 @@ +// 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 BarkBattlePublishedConfigRow { + pub work_id: String, + pub owner_user_id: String, + pub source_draft_id: Option, + pub config_version: u64, + pub ruleset_version: String, + pub difficulty_preset: String, + pub leaderboard_enabled: bool, + pub config_json: String, + pub published_snapshot_json: String, + pub created_at: __sdk::Timestamp, + pub updated_at: __sdk::Timestamp, + pub published_at: __sdk::Timestamp, +} + +impl __sdk::InModule for BarkBattlePublishedConfigRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `BarkBattlePublishedConfigRow`. +/// +/// Provides typed access to columns for query building. +pub struct BarkBattlePublishedConfigRowCols { + pub work_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub source_draft_id: __sdk::__query_builder::Col>, + pub config_version: __sdk::__query_builder::Col, + pub ruleset_version: __sdk::__query_builder::Col, + pub difficulty_preset: __sdk::__query_builder::Col, + pub leaderboard_enabled: __sdk::__query_builder::Col, + pub config_json: __sdk::__query_builder::Col, + pub published_snapshot_json: __sdk::__query_builder::Col, + pub created_at: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, + pub published_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for BarkBattlePublishedConfigRow { + type Cols = BarkBattlePublishedConfigRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + BarkBattlePublishedConfigRowCols { + work_id: __sdk::__query_builder::Col::new(table_name, "work_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + source_draft_id: __sdk::__query_builder::Col::new(table_name, "source_draft_id"), + config_version: __sdk::__query_builder::Col::new(table_name, "config_version"), + ruleset_version: __sdk::__query_builder::Col::new(table_name, "ruleset_version"), + difficulty_preset: __sdk::__query_builder::Col::new(table_name, "difficulty_preset"), + leaderboard_enabled: __sdk::__query_builder::Col::new( + table_name, + "leaderboard_enabled", + ), + config_json: __sdk::__query_builder::Col::new(table_name, "config_json"), + published_snapshot_json: __sdk::__query_builder::Col::new( + table_name, + "published_snapshot_json", + ), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + published_at: __sdk::__query_builder::Col::new(table_name, "published_at"), + } + } +} + +/// Indexed column accessor struct for the table `BarkBattlePublishedConfigRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct BarkBattlePublishedConfigRowIxCols { + pub owner_user_id: __sdk::__query_builder::IxCol, + pub work_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for BarkBattlePublishedConfigRow { + type IxCols = BarkBattlePublishedConfigRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + BarkBattlePublishedConfigRowIxCols { + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + work_id: __sdk::__query_builder::IxCol::new(table_name, "work_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for BarkBattlePublishedConfigRow {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_published_config_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_published_config_table.rs new file mode 100644 index 00000000..a9d7e188 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_published_config_table.rs @@ -0,0 +1,169 @@ +// 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::bark_battle_published_config_row_type::BarkBattlePublishedConfigRow; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `bark_battle_published_config`. +/// +/// Obtain a handle from the [`BarkBattlePublishedConfigTableAccess::bark_battle_published_config`] method on [`super::RemoteTables`], +/// like `ctx.db.bark_battle_published_config()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.bark_battle_published_config().on_insert(...)`. +pub struct BarkBattlePublishedConfigTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `bark_battle_published_config`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait BarkBattlePublishedConfigTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`BarkBattlePublishedConfigTableHandle`], which mediates access to the table `bark_battle_published_config`. + fn bark_battle_published_config(&self) -> BarkBattlePublishedConfigTableHandle<'_>; +} + +impl BarkBattlePublishedConfigTableAccess for super::RemoteTables { + fn bark_battle_published_config(&self) -> BarkBattlePublishedConfigTableHandle<'_> { + BarkBattlePublishedConfigTableHandle { + imp: self + .imp + .get_table::("bark_battle_published_config"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct BarkBattlePublishedConfigInsertCallbackId(__sdk::CallbackId); +pub struct BarkBattlePublishedConfigDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for BarkBattlePublishedConfigTableHandle<'ctx> { + type Row = BarkBattlePublishedConfigRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = BarkBattlePublishedConfigInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BarkBattlePublishedConfigInsertCallbackId { + BarkBattlePublishedConfigInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: BarkBattlePublishedConfigInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = BarkBattlePublishedConfigDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BarkBattlePublishedConfigDeleteCallbackId { + BarkBattlePublishedConfigDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: BarkBattlePublishedConfigDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct BarkBattlePublishedConfigUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for BarkBattlePublishedConfigTableHandle<'ctx> { + type UpdateCallbackId = BarkBattlePublishedConfigUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> BarkBattlePublishedConfigUpdateCallbackId { + BarkBattlePublishedConfigUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: BarkBattlePublishedConfigUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `work_id` unique index on the table `bark_battle_published_config`, +/// which allows point queries on the field of the same name +/// via the [`BarkBattlePublishedConfigWorkIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.bark_battle_published_config().work_id().find(...)`. +pub struct BarkBattlePublishedConfigWorkIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> BarkBattlePublishedConfigTableHandle<'ctx> { + /// Get a handle on the `work_id` unique index on the table `bark_battle_published_config`. + pub fn work_id(&self) -> BarkBattlePublishedConfigWorkIdUnique<'ctx> { + BarkBattlePublishedConfigWorkIdUnique { + imp: self.imp.get_unique_constraint::("work_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> BarkBattlePublishedConfigWorkIdUnique<'ctx> { + /// Find the subscribed row whose `work_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 { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache + .get_or_make_table::("bark_battle_published_config"); + _table.add_unique_constraint::("work_id", |row| &row.work_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse( + "TableUpdate", + "TableUpdate", + ) + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `BarkBattlePublishedConfigRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait bark_battle_published_configQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `BarkBattlePublishedConfigRow`. + fn bark_battle_published_config( + &self, + ) -> __sdk::__query_builder::Table; +} + +impl bark_battle_published_configQueryTableAccess for __sdk::QueryTableAccessor { + fn bark_battle_published_config( + &self, + ) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("bark_battle_published_config") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_run_finish_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_run_finish_input_type.rs new file mode 100644 index 00000000..d8ac7dd0 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_run_finish_input_type.rs @@ -0,0 +1,32 @@ +// 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 BarkBattleRunFinishInput { + pub run_id: String, + pub run_token: String, + pub owner_user_id: String, + pub work_id: String, + pub config_version: u64, + pub ruleset_version: String, + pub difficulty_preset: String, + pub client_finished_at_micros: i64, + pub server_finished_at_micros: i64, + pub duration_ms: u64, + pub trigger_count: u64, + pub max_volume_millis: u32, + pub average_volume_millis: u32, + pub final_energy_millis: u32, + pub opponent_final_energy_millis: u32, + pub max_combo: u32, + pub metrics_json: String, + pub derived_metrics_json: String, +} + +impl __sdk::InModule for BarkBattleRunFinishInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_run_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_run_get_input_type.rs new file mode 100644 index 00000000..243bedca --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_run_get_input_type.rs @@ -0,0 +1,16 @@ +// 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 BarkBattleRunGetInput { + pub run_id: String, + pub owner_user_id: String, +} + +impl __sdk::InModule for BarkBattleRunGetInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_run_start_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_run_start_input_type.rs new file mode 100644 index 00000000..3dee4380 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_run_start_input_type.rs @@ -0,0 +1,23 @@ +// 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 BarkBattleRunStartInput { + pub run_id: String, + pub run_token: String, + pub owner_user_id: String, + pub work_id: String, + pub config_version: u64, + pub ruleset_version: String, + pub difficulty_preset: String, + pub client_started_at_micros: i64, + pub server_started_at_micros: i64, +} + +impl __sdk::InModule for BarkBattleRunStartInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_runtime_config_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_runtime_config_get_input_type.rs new file mode 100644 index 00000000..0acef769 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_runtime_config_get_input_type.rs @@ -0,0 +1,16 @@ +// 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 BarkBattleRuntimeConfigGetInput { + pub work_id: String, + pub owner_user_id: Option, +} + +impl __sdk::InModule for BarkBattleRuntimeConfigGetInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_runtime_run_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_runtime_run_row_type.rs new file mode 100644 index 00000000..24edf9c6 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_runtime_run_row_type.rs @@ -0,0 +1,127 @@ +// 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 BarkBattleRuntimeRunRow { + pub run_id: String, + pub run_token_hash: String, + pub owner_user_id: String, + pub work_id: String, + pub config_version: u64, + pub ruleset_version: String, + pub difficulty_preset: String, + pub leaderboard_enabled: bool, + pub status: String, + pub client_started_at_micros: i64, + pub server_started_at: __sdk::Timestamp, + pub client_finished_at_micros: Option, + pub server_finished_at: Option<__sdk::Timestamp>, + pub metrics_json: String, + pub server_result: Option, + pub validation_status: String, + pub anti_cheat_flags_json: String, + pub leaderboard_score: Option, + pub score_id: Option, + pub created_at: __sdk::Timestamp, + pub updated_at: __sdk::Timestamp, +} + +impl __sdk::InModule for BarkBattleRuntimeRunRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `BarkBattleRuntimeRunRow`. +/// +/// Provides typed access to columns for query building. +pub struct BarkBattleRuntimeRunRowCols { + pub run_id: __sdk::__query_builder::Col, + pub run_token_hash: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub work_id: __sdk::__query_builder::Col, + pub config_version: __sdk::__query_builder::Col, + pub ruleset_version: __sdk::__query_builder::Col, + pub difficulty_preset: __sdk::__query_builder::Col, + pub leaderboard_enabled: __sdk::__query_builder::Col, + pub status: __sdk::__query_builder::Col, + pub client_started_at_micros: __sdk::__query_builder::Col, + pub server_started_at: __sdk::__query_builder::Col, + pub client_finished_at_micros: + __sdk::__query_builder::Col>, + pub server_finished_at: + __sdk::__query_builder::Col>, + pub metrics_json: __sdk::__query_builder::Col, + pub server_result: __sdk::__query_builder::Col>, + pub validation_status: __sdk::__query_builder::Col, + pub anti_cheat_flags_json: __sdk::__query_builder::Col, + pub leaderboard_score: __sdk::__query_builder::Col>, + pub score_id: __sdk::__query_builder::Col>, + pub created_at: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for BarkBattleRuntimeRunRow { + type Cols = BarkBattleRuntimeRunRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + BarkBattleRuntimeRunRowCols { + run_id: __sdk::__query_builder::Col::new(table_name, "run_id"), + run_token_hash: __sdk::__query_builder::Col::new(table_name, "run_token_hash"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + work_id: __sdk::__query_builder::Col::new(table_name, "work_id"), + config_version: __sdk::__query_builder::Col::new(table_name, "config_version"), + ruleset_version: __sdk::__query_builder::Col::new(table_name, "ruleset_version"), + difficulty_preset: __sdk::__query_builder::Col::new(table_name, "difficulty_preset"), + leaderboard_enabled: __sdk::__query_builder::Col::new( + table_name, + "leaderboard_enabled", + ), + status: __sdk::__query_builder::Col::new(table_name, "status"), + client_started_at_micros: __sdk::__query_builder::Col::new( + table_name, + "client_started_at_micros", + ), + server_started_at: __sdk::__query_builder::Col::new(table_name, "server_started_at"), + client_finished_at_micros: __sdk::__query_builder::Col::new( + table_name, + "client_finished_at_micros", + ), + server_finished_at: __sdk::__query_builder::Col::new(table_name, "server_finished_at"), + metrics_json: __sdk::__query_builder::Col::new(table_name, "metrics_json"), + server_result: __sdk::__query_builder::Col::new(table_name, "server_result"), + validation_status: __sdk::__query_builder::Col::new(table_name, "validation_status"), + anti_cheat_flags_json: __sdk::__query_builder::Col::new( + table_name, + "anti_cheat_flags_json", + ), + leaderboard_score: __sdk::__query_builder::Col::new(table_name, "leaderboard_score"), + score_id: __sdk::__query_builder::Col::new(table_name, "score_id"), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + } + } +} + +/// Indexed column accessor struct for the table `BarkBattleRuntimeRunRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct BarkBattleRuntimeRunRowIxCols { + pub owner_user_id: __sdk::__query_builder::IxCol, + pub run_id: __sdk::__query_builder::IxCol, + pub work_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for BarkBattleRuntimeRunRow { + type IxCols = BarkBattleRuntimeRunRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + BarkBattleRuntimeRunRowIxCols { + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + run_id: __sdk::__query_builder::IxCol::new(table_name, "run_id"), + work_id: __sdk::__query_builder::IxCol::new(table_name, "work_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for BarkBattleRuntimeRunRow {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_runtime_run_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_runtime_run_table.rs new file mode 100644 index 00000000..fad71366 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_runtime_run_table.rs @@ -0,0 +1,162 @@ +// 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::bark_battle_runtime_run_row_type::BarkBattleRuntimeRunRow; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `bark_battle_runtime_run`. +/// +/// Obtain a handle from the [`BarkBattleRuntimeRunTableAccess::bark_battle_runtime_run`] method on [`super::RemoteTables`], +/// like `ctx.db.bark_battle_runtime_run()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.bark_battle_runtime_run().on_insert(...)`. +pub struct BarkBattleRuntimeRunTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `bark_battle_runtime_run`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait BarkBattleRuntimeRunTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`BarkBattleRuntimeRunTableHandle`], which mediates access to the table `bark_battle_runtime_run`. + fn bark_battle_runtime_run(&self) -> BarkBattleRuntimeRunTableHandle<'_>; +} + +impl BarkBattleRuntimeRunTableAccess for super::RemoteTables { + fn bark_battle_runtime_run(&self) -> BarkBattleRuntimeRunTableHandle<'_> { + BarkBattleRuntimeRunTableHandle { + imp: self + .imp + .get_table::("bark_battle_runtime_run"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct BarkBattleRuntimeRunInsertCallbackId(__sdk::CallbackId); +pub struct BarkBattleRuntimeRunDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for BarkBattleRuntimeRunTableHandle<'ctx> { + type Row = BarkBattleRuntimeRunRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = BarkBattleRuntimeRunInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BarkBattleRuntimeRunInsertCallbackId { + BarkBattleRuntimeRunInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: BarkBattleRuntimeRunInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = BarkBattleRuntimeRunDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BarkBattleRuntimeRunDeleteCallbackId { + BarkBattleRuntimeRunDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: BarkBattleRuntimeRunDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct BarkBattleRuntimeRunUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for BarkBattleRuntimeRunTableHandle<'ctx> { + type UpdateCallbackId = BarkBattleRuntimeRunUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> BarkBattleRuntimeRunUpdateCallbackId { + BarkBattleRuntimeRunUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: BarkBattleRuntimeRunUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `run_id` unique index on the table `bark_battle_runtime_run`, +/// which allows point queries on the field of the same name +/// via the [`BarkBattleRuntimeRunRunIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.bark_battle_runtime_run().run_id().find(...)`. +pub struct BarkBattleRuntimeRunRunIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> BarkBattleRuntimeRunTableHandle<'ctx> { + /// Get a handle on the `run_id` unique index on the table `bark_battle_runtime_run`. + pub fn run_id(&self) -> BarkBattleRuntimeRunRunIdUnique<'ctx> { + BarkBattleRuntimeRunRunIdUnique { + imp: self.imp.get_unique_constraint::("run_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> BarkBattleRuntimeRunRunIdUnique<'ctx> { + /// Find the subscribed row whose `run_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 { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = + client_cache.get_or_make_table::("bark_battle_runtime_run"); + _table.add_unique_constraint::("run_id", |row| &row.run_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `BarkBattleRuntimeRunRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait bark_battle_runtime_runQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `BarkBattleRuntimeRunRow`. + fn bark_battle_runtime_run(&self) -> __sdk::__query_builder::Table; +} + +impl bark_battle_runtime_runQueryTableAccess for __sdk::QueryTableAccessor { + fn bark_battle_runtime_run(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("bark_battle_runtime_run") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_score_record_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_score_record_row_type.rs new file mode 100644 index 00000000..8c8703b4 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_score_record_row_type.rs @@ -0,0 +1,106 @@ +// 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 BarkBattleScoreRecordRow { + pub score_id: String, + pub owner_user_id: String, + pub work_id: String, + pub run_id: String, + pub config_version: u64, + pub ruleset_version: String, + pub difficulty_preset: String, + pub leaderboard_enabled: bool, + pub metrics_json: String, + pub derived_metrics_json: String, + pub server_result: String, + pub validation_status: String, + pub anti_cheat_flags_json: String, + pub leaderboard_score: Option, + pub recorded_at: __sdk::Timestamp, +} + +impl __sdk::InModule for BarkBattleScoreRecordRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `BarkBattleScoreRecordRow`. +/// +/// Provides typed access to columns for query building. +pub struct BarkBattleScoreRecordRowCols { + pub score_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub work_id: __sdk::__query_builder::Col, + pub run_id: __sdk::__query_builder::Col, + pub config_version: __sdk::__query_builder::Col, + pub ruleset_version: __sdk::__query_builder::Col, + pub difficulty_preset: __sdk::__query_builder::Col, + pub leaderboard_enabled: __sdk::__query_builder::Col, + pub metrics_json: __sdk::__query_builder::Col, + pub derived_metrics_json: __sdk::__query_builder::Col, + pub server_result: __sdk::__query_builder::Col, + pub validation_status: __sdk::__query_builder::Col, + pub anti_cheat_flags_json: __sdk::__query_builder::Col, + pub leaderboard_score: __sdk::__query_builder::Col>, + pub recorded_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for BarkBattleScoreRecordRow { + type Cols = BarkBattleScoreRecordRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + BarkBattleScoreRecordRowCols { + score_id: __sdk::__query_builder::Col::new(table_name, "score_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + work_id: __sdk::__query_builder::Col::new(table_name, "work_id"), + run_id: __sdk::__query_builder::Col::new(table_name, "run_id"), + config_version: __sdk::__query_builder::Col::new(table_name, "config_version"), + ruleset_version: __sdk::__query_builder::Col::new(table_name, "ruleset_version"), + difficulty_preset: __sdk::__query_builder::Col::new(table_name, "difficulty_preset"), + leaderboard_enabled: __sdk::__query_builder::Col::new( + table_name, + "leaderboard_enabled", + ), + metrics_json: __sdk::__query_builder::Col::new(table_name, "metrics_json"), + derived_metrics_json: __sdk::__query_builder::Col::new( + table_name, + "derived_metrics_json", + ), + server_result: __sdk::__query_builder::Col::new(table_name, "server_result"), + validation_status: __sdk::__query_builder::Col::new(table_name, "validation_status"), + anti_cheat_flags_json: __sdk::__query_builder::Col::new( + table_name, + "anti_cheat_flags_json", + ), + leaderboard_score: __sdk::__query_builder::Col::new(table_name, "leaderboard_score"), + recorded_at: __sdk::__query_builder::Col::new(table_name, "recorded_at"), + } + } +} + +/// Indexed column accessor struct for the table `BarkBattleScoreRecordRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct BarkBattleScoreRecordRowIxCols { + pub owner_user_id: __sdk::__query_builder::IxCol, + pub run_id: __sdk::__query_builder::IxCol, + pub score_id: __sdk::__query_builder::IxCol, + pub work_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for BarkBattleScoreRecordRow { + type IxCols = BarkBattleScoreRecordRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + BarkBattleScoreRecordRowIxCols { + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + run_id: __sdk::__query_builder::IxCol::new(table_name, "run_id"), + score_id: __sdk::__query_builder::IxCol::new(table_name, "score_id"), + work_id: __sdk::__query_builder::IxCol::new(table_name, "work_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for BarkBattleScoreRecordRow {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_score_record_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_score_record_table.rs new file mode 100644 index 00000000..baee1a30 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_score_record_table.rs @@ -0,0 +1,162 @@ +// 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::bark_battle_score_record_row_type::BarkBattleScoreRecordRow; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `bark_battle_score_record`. +/// +/// Obtain a handle from the [`BarkBattleScoreRecordTableAccess::bark_battle_score_record`] method on [`super::RemoteTables`], +/// like `ctx.db.bark_battle_score_record()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.bark_battle_score_record().on_insert(...)`. +pub struct BarkBattleScoreRecordTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `bark_battle_score_record`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait BarkBattleScoreRecordTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`BarkBattleScoreRecordTableHandle`], which mediates access to the table `bark_battle_score_record`. + fn bark_battle_score_record(&self) -> BarkBattleScoreRecordTableHandle<'_>; +} + +impl BarkBattleScoreRecordTableAccess for super::RemoteTables { + fn bark_battle_score_record(&self) -> BarkBattleScoreRecordTableHandle<'_> { + BarkBattleScoreRecordTableHandle { + imp: self + .imp + .get_table::("bark_battle_score_record"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct BarkBattleScoreRecordInsertCallbackId(__sdk::CallbackId); +pub struct BarkBattleScoreRecordDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for BarkBattleScoreRecordTableHandle<'ctx> { + type Row = BarkBattleScoreRecordRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = BarkBattleScoreRecordInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BarkBattleScoreRecordInsertCallbackId { + BarkBattleScoreRecordInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: BarkBattleScoreRecordInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = BarkBattleScoreRecordDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BarkBattleScoreRecordDeleteCallbackId { + BarkBattleScoreRecordDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: BarkBattleScoreRecordDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct BarkBattleScoreRecordUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for BarkBattleScoreRecordTableHandle<'ctx> { + type UpdateCallbackId = BarkBattleScoreRecordUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> BarkBattleScoreRecordUpdateCallbackId { + BarkBattleScoreRecordUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: BarkBattleScoreRecordUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `score_id` unique index on the table `bark_battle_score_record`, +/// which allows point queries on the field of the same name +/// via the [`BarkBattleScoreRecordScoreIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.bark_battle_score_record().score_id().find(...)`. +pub struct BarkBattleScoreRecordScoreIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> BarkBattleScoreRecordTableHandle<'ctx> { + /// Get a handle on the `score_id` unique index on the table `bark_battle_score_record`. + pub fn score_id(&self) -> BarkBattleScoreRecordScoreIdUnique<'ctx> { + BarkBattleScoreRecordScoreIdUnique { + imp: self.imp.get_unique_constraint::("score_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> BarkBattleScoreRecordScoreIdUnique<'ctx> { + /// Find the subscribed row whose `score_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 { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = + client_cache.get_or_make_table::("bark_battle_score_record"); + _table.add_unique_constraint::("score_id", |row| &row.score_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `BarkBattleScoreRecordRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait bark_battle_score_recordQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `BarkBattleScoreRecordRow`. + fn bark_battle_score_record(&self) -> __sdk::__query_builder::Table; +} + +impl bark_battle_score_recordQueryTableAccess for __sdk::QueryTableAccessor { + fn bark_battle_score_record(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("bark_battle_score_record") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_work_publish_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_work_publish_input_type.rs new file mode 100644 index 00000000..d3e50694 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_work_publish_input_type.rs @@ -0,0 +1,19 @@ +// 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 BarkBattleWorkPublishInput { + pub draft_id: String, + pub owner_user_id: String, + pub work_id: String, + pub published_snapshot_json: Option, + pub published_at_micros: i64, +} + +impl __sdk::InModule for BarkBattleWorkPublishInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_work_stats_projection_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_work_stats_projection_row_type.rs new file mode 100644 index 00000000..2d116395 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_work_stats_projection_row_type.rs @@ -0,0 +1,111 @@ +// 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 BarkBattleWorkStatsProjectionRow { + pub work_id: String, + pub owner_user_id: String, + pub play_count: u64, + pub finished_count: u64, + pub accepted_score_count: u64, + pub leaderboard_entry_count: u64, + pub best_leaderboard_score: Option, + pub best_score_id: Option, + pub best_run_id: Option, + pub average_final_energy: f32, + pub average_trigger_count: f32, + pub last_finished_at_micros: Option, + pub stats_json: String, + pub updated_at: __sdk::Timestamp, +} + +impl __sdk::InModule for BarkBattleWorkStatsProjectionRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `BarkBattleWorkStatsProjectionRow`. +/// +/// Provides typed access to columns for query building. +pub struct BarkBattleWorkStatsProjectionRowCols { + pub work_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub play_count: __sdk::__query_builder::Col, + pub finished_count: __sdk::__query_builder::Col, + pub accepted_score_count: __sdk::__query_builder::Col, + pub leaderboard_entry_count: __sdk::__query_builder::Col, + pub best_leaderboard_score: + __sdk::__query_builder::Col>, + pub best_score_id: + __sdk::__query_builder::Col>, + pub best_run_id: __sdk::__query_builder::Col>, + pub average_final_energy: __sdk::__query_builder::Col, + pub average_trigger_count: __sdk::__query_builder::Col, + pub last_finished_at_micros: + __sdk::__query_builder::Col>, + pub stats_json: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for BarkBattleWorkStatsProjectionRow { + type Cols = BarkBattleWorkStatsProjectionRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + BarkBattleWorkStatsProjectionRowCols { + work_id: __sdk::__query_builder::Col::new(table_name, "work_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + play_count: __sdk::__query_builder::Col::new(table_name, "play_count"), + finished_count: __sdk::__query_builder::Col::new(table_name, "finished_count"), + accepted_score_count: __sdk::__query_builder::Col::new( + table_name, + "accepted_score_count", + ), + leaderboard_entry_count: __sdk::__query_builder::Col::new( + table_name, + "leaderboard_entry_count", + ), + best_leaderboard_score: __sdk::__query_builder::Col::new( + table_name, + "best_leaderboard_score", + ), + best_score_id: __sdk::__query_builder::Col::new(table_name, "best_score_id"), + best_run_id: __sdk::__query_builder::Col::new(table_name, "best_run_id"), + average_final_energy: __sdk::__query_builder::Col::new( + table_name, + "average_final_energy", + ), + average_trigger_count: __sdk::__query_builder::Col::new( + table_name, + "average_trigger_count", + ), + last_finished_at_micros: __sdk::__query_builder::Col::new( + table_name, + "last_finished_at_micros", + ), + stats_json: __sdk::__query_builder::Col::new(table_name, "stats_json"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + } + } +} + +/// Indexed column accessor struct for the table `BarkBattleWorkStatsProjectionRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct BarkBattleWorkStatsProjectionRowIxCols { + pub owner_user_id: __sdk::__query_builder::IxCol, + pub work_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for BarkBattleWorkStatsProjectionRow { + type IxCols = BarkBattleWorkStatsProjectionRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + BarkBattleWorkStatsProjectionRowIxCols { + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + work_id: __sdk::__query_builder::IxCol::new(table_name, "work_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for BarkBattleWorkStatsProjectionRow {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_work_stats_projection_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_work_stats_projection_table.rs new file mode 100644 index 00000000..66cf17cb --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_work_stats_projection_table.rs @@ -0,0 +1,169 @@ +// 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::bark_battle_work_stats_projection_row_type::BarkBattleWorkStatsProjectionRow; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `bark_battle_work_stats_projection`. +/// +/// Obtain a handle from the [`BarkBattleWorkStatsProjectionTableAccess::bark_battle_work_stats_projection`] method on [`super::RemoteTables`], +/// like `ctx.db.bark_battle_work_stats_projection()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.bark_battle_work_stats_projection().on_insert(...)`. +pub struct BarkBattleWorkStatsProjectionTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `bark_battle_work_stats_projection`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait BarkBattleWorkStatsProjectionTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`BarkBattleWorkStatsProjectionTableHandle`], which mediates access to the table `bark_battle_work_stats_projection`. + fn bark_battle_work_stats_projection(&self) -> BarkBattleWorkStatsProjectionTableHandle<'_>; +} + +impl BarkBattleWorkStatsProjectionTableAccess for super::RemoteTables { + fn bark_battle_work_stats_projection(&self) -> BarkBattleWorkStatsProjectionTableHandle<'_> { + BarkBattleWorkStatsProjectionTableHandle { + imp: self + .imp + .get_table::("bark_battle_work_stats_projection"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct BarkBattleWorkStatsProjectionInsertCallbackId(__sdk::CallbackId); +pub struct BarkBattleWorkStatsProjectionDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for BarkBattleWorkStatsProjectionTableHandle<'ctx> { + type Row = BarkBattleWorkStatsProjectionRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = BarkBattleWorkStatsProjectionInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BarkBattleWorkStatsProjectionInsertCallbackId { + BarkBattleWorkStatsProjectionInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: BarkBattleWorkStatsProjectionInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = BarkBattleWorkStatsProjectionDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BarkBattleWorkStatsProjectionDeleteCallbackId { + BarkBattleWorkStatsProjectionDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: BarkBattleWorkStatsProjectionDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct BarkBattleWorkStatsProjectionUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for BarkBattleWorkStatsProjectionTableHandle<'ctx> { + type UpdateCallbackId = BarkBattleWorkStatsProjectionUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> BarkBattleWorkStatsProjectionUpdateCallbackId { + BarkBattleWorkStatsProjectionUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: BarkBattleWorkStatsProjectionUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `work_id` unique index on the table `bark_battle_work_stats_projection`, +/// which allows point queries on the field of the same name +/// via the [`BarkBattleWorkStatsProjectionWorkIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.bark_battle_work_stats_projection().work_id().find(...)`. +pub struct BarkBattleWorkStatsProjectionWorkIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> BarkBattleWorkStatsProjectionTableHandle<'ctx> { + /// Get a handle on the `work_id` unique index on the table `bark_battle_work_stats_projection`. + pub fn work_id(&self) -> BarkBattleWorkStatsProjectionWorkIdUnique<'ctx> { + BarkBattleWorkStatsProjectionWorkIdUnique { + imp: self.imp.get_unique_constraint::("work_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> BarkBattleWorkStatsProjectionWorkIdUnique<'ctx> { + /// Find the subscribed row whose `work_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 { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache + .get_or_make_table::("bark_battle_work_stats_projection"); + _table.add_unique_constraint::("work_id", |row| &row.work_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse( + "TableUpdate", + "TableUpdate", + ) + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `BarkBattleWorkStatsProjectionRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait bark_battle_work_stats_projectionQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `BarkBattleWorkStatsProjectionRow`. + fn bark_battle_work_stats_projection( + &self, + ) -> __sdk::__query_builder::Table; +} + +impl bark_battle_work_stats_projectionQueryTableAccess for __sdk::QueryTableAccessor { + fn bark_battle_work_stats_projection( + &self, + ) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("bark_battle_work_stats_projection") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/create_bark_battle_draft_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/create_bark_battle_draft_procedure.rs new file mode 100644 index 00000000..87e104b6 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/create_bark_battle_draft_procedure.rs @@ -0,0 +1,59 @@ +// 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::bark_battle_draft_create_input_type::BarkBattleDraftCreateInput; +use super::bark_battle_procedure_result_type::BarkBattleProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct CreateBarkBattleDraftArgs { + pub input: BarkBattleDraftCreateInput, +} + +impl __sdk::InModule for CreateBarkBattleDraftArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `create_bark_battle_draft`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait create_bark_battle_draft { + fn create_bark_battle_draft(&self, input: BarkBattleDraftCreateInput) { + self.create_bark_battle_draft_then(input, |_, _| {}); + } + + fn create_bark_battle_draft_then( + &self, + input: BarkBattleDraftCreateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl create_bark_battle_draft for super::RemoteProcedures { + fn create_bark_battle_draft_then( + &self, + input: BarkBattleDraftCreateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, BarkBattleProcedureResult>( + "create_bark_battle_draft", + CreateBarkBattleDraftArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/finish_bark_battle_run_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/finish_bark_battle_run_procedure.rs new file mode 100644 index 00000000..28fc7ef3 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/finish_bark_battle_run_procedure.rs @@ -0,0 +1,59 @@ +// 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::bark_battle_procedure_result_type::BarkBattleProcedureResult; +use super::bark_battle_run_finish_input_type::BarkBattleRunFinishInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct FinishBarkBattleRunArgs { + pub input: BarkBattleRunFinishInput, +} + +impl __sdk::InModule for FinishBarkBattleRunArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `finish_bark_battle_run`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait finish_bark_battle_run { + fn finish_bark_battle_run(&self, input: BarkBattleRunFinishInput) { + self.finish_bark_battle_run_then(input, |_, _| {}); + } + + fn finish_bark_battle_run_then( + &self, + input: BarkBattleRunFinishInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl finish_bark_battle_run for super::RemoteProcedures { + fn finish_bark_battle_run_then( + &self, + input: BarkBattleRunFinishInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, BarkBattleProcedureResult>( + "finish_bark_battle_run", + FinishBarkBattleRunArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_bark_battle_run_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_bark_battle_run_procedure.rs new file mode 100644 index 00000000..bec98ca8 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_bark_battle_run_procedure.rs @@ -0,0 +1,59 @@ +// 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::bark_battle_procedure_result_type::BarkBattleProcedureResult; +use super::bark_battle_run_get_input_type::BarkBattleRunGetInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct GetBarkBattleRunArgs { + pub input: BarkBattleRunGetInput, +} + +impl __sdk::InModule for GetBarkBattleRunArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_bark_battle_run`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_bark_battle_run { + fn get_bark_battle_run(&self, input: BarkBattleRunGetInput) { + self.get_bark_battle_run_then(input, |_, _| {}); + } + + fn get_bark_battle_run_then( + &self, + input: BarkBattleRunGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl get_bark_battle_run for super::RemoteProcedures { + fn get_bark_battle_run_then( + &self, + input: BarkBattleRunGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, BarkBattleProcedureResult>( + "get_bark_battle_run", + GetBarkBattleRunArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_bark_battle_runtime_config_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_bark_battle_runtime_config_procedure.rs new file mode 100644 index 00000000..6e4364f2 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_bark_battle_runtime_config_procedure.rs @@ -0,0 +1,59 @@ +// 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::bark_battle_procedure_result_type::BarkBattleProcedureResult; +use super::bark_battle_runtime_config_get_input_type::BarkBattleRuntimeConfigGetInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct GetBarkBattleRuntimeConfigArgs { + pub input: BarkBattleRuntimeConfigGetInput, +} + +impl __sdk::InModule for GetBarkBattleRuntimeConfigArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_bark_battle_runtime_config`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_bark_battle_runtime_config { + fn get_bark_battle_runtime_config(&self, input: BarkBattleRuntimeConfigGetInput) { + self.get_bark_battle_runtime_config_then(input, |_, _| {}); + } + + fn get_bark_battle_runtime_config_then( + &self, + input: BarkBattleRuntimeConfigGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl get_bark_battle_runtime_config for super::RemoteProcedures { + fn get_bark_battle_runtime_config_then( + &self, + input: BarkBattleRuntimeConfigGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, BarkBattleProcedureResult>( + "get_bark_battle_runtime_config", + GetBarkBattleRuntimeConfigArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/mod.rs b/server-rs/crates/spacetime-client/src/module_bindings/mod.rs index 5898b71c..7fee00ac 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/mod.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/mod.rs @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 2.1.0 (commit 6981f48b4bc1a71c8dd9bdfe5a2c343f6370243d). +// This was generated using spacetimedb cli version 2.2.0 (commit eb11e2f5c41dce6979715ad407996270d61329f6). #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; @@ -90,6 +90,28 @@ pub mod auth_store_snapshot_table; pub mod auth_store_snapshot_type; pub mod auth_store_snapshot_upsert_input_type; pub mod authorize_database_migration_operator_procedure; +pub mod bark_battle_draft_config_row_type; +pub mod bark_battle_draft_config_table; +pub mod bark_battle_draft_config_upsert_input_type; +pub mod bark_battle_draft_create_input_type; +pub mod bark_battle_leaderboard_entry_row_type; +pub mod bark_battle_leaderboard_entry_table; +pub mod bark_battle_personal_best_projection_row_type; +pub mod bark_battle_personal_best_projection_table; +pub mod bark_battle_procedure_result_type; +pub mod bark_battle_published_config_row_type; +pub mod bark_battle_published_config_table; +pub mod bark_battle_run_finish_input_type; +pub mod bark_battle_run_get_input_type; +pub mod bark_battle_run_start_input_type; +pub mod bark_battle_runtime_config_get_input_type; +pub mod bark_battle_runtime_run_row_type; +pub mod bark_battle_runtime_run_table; +pub mod bark_battle_score_record_row_type; +pub mod bark_battle_score_record_table; +pub mod bark_battle_work_publish_input_type; +pub mod bark_battle_work_stats_projection_row_type; +pub mod bark_battle_work_stats_projection_table; pub mod battle_mode_type; pub mod battle_state_input_type; pub mod battle_state_procedure_result_type; @@ -179,6 +201,7 @@ pub mod continue_story_and_return_procedure; pub mod continue_story_reducer; pub mod create_ai_task_and_return_procedure; pub mod create_ai_task_reducer; +pub mod create_bark_battle_draft_procedure; pub mod create_battle_state_and_return_procedure; pub mod create_battle_state_reducer; pub mod create_big_fish_session_procedure; @@ -296,10 +319,13 @@ pub mod finalize_match_3_d_agent_message_turn_procedure; pub mod finalize_puzzle_agent_message_turn_procedure; pub mod finalize_square_hole_agent_message_turn_procedure; pub mod finalize_visual_novel_agent_message_turn_procedure; +pub mod finish_bark_battle_run_procedure; pub mod finish_match_3_d_time_up_procedure; pub mod finish_square_hole_time_up_procedure; pub mod generate_big_fish_asset_procedure; pub mod get_auth_store_snapshot_procedure; +pub mod get_bark_battle_run_procedure; +pub mod get_bark_battle_runtime_config_procedure; pub mod get_battle_state_procedure; pub mod get_big_fish_run_procedure; pub mod get_big_fish_session_procedure; @@ -450,6 +476,7 @@ pub mod public_work_like_table; pub mod public_work_like_type; pub mod public_work_play_daily_stat_table; pub mod public_work_play_daily_stat_type; +pub mod publish_bark_battle_work_procedure; pub mod publish_big_fish_game_procedure; pub mod publish_custom_world_profile_and_return_procedure; pub mod publish_custom_world_profile_reducer; @@ -712,6 +739,7 @@ pub mod square_hole_works_list_input_type; pub mod square_hole_works_procedure_result_type; pub mod start_ai_task_reducer; pub mod start_ai_task_stage_reducer; +pub mod start_bark_battle_run_procedure; pub mod start_big_fish_run_procedure; pub mod start_match_3_d_run_procedure; pub mod start_puzzle_run_procedure; @@ -756,6 +784,7 @@ pub mod turn_in_quest_reducer; pub mod unequip_inventory_item_input_type; pub mod unpublish_custom_world_profile_and_return_procedure; pub mod unpublish_custom_world_profile_reducer; +pub mod update_bark_battle_draft_config_procedure; pub mod update_match_3_d_work_procedure; pub mod update_puzzle_run_pause_procedure; pub mod update_puzzle_work_procedure; @@ -898,6 +927,28 @@ pub use auth_store_snapshot_table::*; pub use auth_store_snapshot_type::AuthStoreSnapshot; pub use auth_store_snapshot_upsert_input_type::AuthStoreSnapshotUpsertInput; pub use authorize_database_migration_operator_procedure::authorize_database_migration_operator; +pub use bark_battle_draft_config_row_type::BarkBattleDraftConfigRow; +pub use bark_battle_draft_config_table::*; +pub use bark_battle_draft_config_upsert_input_type::BarkBattleDraftConfigUpsertInput; +pub use bark_battle_draft_create_input_type::BarkBattleDraftCreateInput; +pub use bark_battle_leaderboard_entry_row_type::BarkBattleLeaderboardEntryRow; +pub use bark_battle_leaderboard_entry_table::*; +pub use bark_battle_personal_best_projection_row_type::BarkBattlePersonalBestProjectionRow; +pub use bark_battle_personal_best_projection_table::*; +pub use bark_battle_procedure_result_type::BarkBattleProcedureResult; +pub use bark_battle_published_config_row_type::BarkBattlePublishedConfigRow; +pub use bark_battle_published_config_table::*; +pub use bark_battle_run_finish_input_type::BarkBattleRunFinishInput; +pub use bark_battle_run_get_input_type::BarkBattleRunGetInput; +pub use bark_battle_run_start_input_type::BarkBattleRunStartInput; +pub use bark_battle_runtime_config_get_input_type::BarkBattleRuntimeConfigGetInput; +pub use bark_battle_runtime_run_row_type::BarkBattleRuntimeRunRow; +pub use bark_battle_runtime_run_table::*; +pub use bark_battle_score_record_row_type::BarkBattleScoreRecordRow; +pub use bark_battle_score_record_table::*; +pub use bark_battle_work_publish_input_type::BarkBattleWorkPublishInput; +pub use bark_battle_work_stats_projection_row_type::BarkBattleWorkStatsProjectionRow; +pub use bark_battle_work_stats_projection_table::*; pub use battle_mode_type::BattleMode; pub use battle_state_input_type::BattleStateInput; pub use battle_state_procedure_result_type::BattleStateProcedureResult; @@ -987,6 +1038,7 @@ pub use continue_story_and_return_procedure::continue_story_and_return; pub use continue_story_reducer::continue_story; pub use create_ai_task_and_return_procedure::create_ai_task_and_return; pub use create_ai_task_reducer::create_ai_task; +pub use create_bark_battle_draft_procedure::create_bark_battle_draft; pub use create_battle_state_and_return_procedure::create_battle_state_and_return; pub use create_battle_state_reducer::create_battle_state; pub use create_big_fish_session_procedure::create_big_fish_session; @@ -1104,10 +1156,13 @@ pub use finalize_match_3_d_agent_message_turn_procedure::finalize_match_3_d_agen pub use finalize_puzzle_agent_message_turn_procedure::finalize_puzzle_agent_message_turn; pub use finalize_square_hole_agent_message_turn_procedure::finalize_square_hole_agent_message_turn; pub use finalize_visual_novel_agent_message_turn_procedure::finalize_visual_novel_agent_message_turn; +pub use finish_bark_battle_run_procedure::finish_bark_battle_run; pub use finish_match_3_d_time_up_procedure::finish_match_3_d_time_up; pub use finish_square_hole_time_up_procedure::finish_square_hole_time_up; pub use generate_big_fish_asset_procedure::generate_big_fish_asset; pub use get_auth_store_snapshot_procedure::get_auth_store_snapshot; +pub use get_bark_battle_run_procedure::get_bark_battle_run; +pub use get_bark_battle_runtime_config_procedure::get_bark_battle_runtime_config; pub use get_battle_state_procedure::get_battle_state; pub use get_big_fish_run_procedure::get_big_fish_run; pub use get_big_fish_session_procedure::get_big_fish_session; @@ -1258,6 +1313,7 @@ pub use public_work_like_table::*; pub use public_work_like_type::PublicWorkLike; pub use public_work_play_daily_stat_table::*; pub use public_work_play_daily_stat_type::PublicWorkPlayDailyStat; +pub use publish_bark_battle_work_procedure::publish_bark_battle_work; pub use publish_big_fish_game_procedure::publish_big_fish_game; pub use publish_custom_world_profile_and_return_procedure::publish_custom_world_profile_and_return; pub use publish_custom_world_profile_reducer::publish_custom_world_profile; @@ -1520,6 +1576,7 @@ pub use square_hole_works_list_input_type::SquareHoleWorksListInput; pub use square_hole_works_procedure_result_type::SquareHoleWorksProcedureResult; pub use start_ai_task_reducer::start_ai_task; pub use start_ai_task_stage_reducer::start_ai_task_stage; +pub use start_bark_battle_run_procedure::start_bark_battle_run; pub use start_big_fish_run_procedure::start_big_fish_run; pub use start_match_3_d_run_procedure::start_match_3_d_run; pub use start_puzzle_run_procedure::start_puzzle_run; @@ -1564,6 +1621,7 @@ pub use turn_in_quest_reducer::turn_in_quest; pub use unequip_inventory_item_input_type::UnequipInventoryItemInput; pub use unpublish_custom_world_profile_and_return_procedure::unpublish_custom_world_profile_and_return; pub use unpublish_custom_world_profile_reducer::unpublish_custom_world_profile; +pub use update_bark_battle_draft_config_procedure::update_bark_battle_draft_config; pub use update_match_3_d_work_procedure::update_match_3_d_work; pub use update_puzzle_run_pause_procedure::update_puzzle_run_pause; pub use update_puzzle_work_procedure::update_puzzle_work; @@ -1905,6 +1963,13 @@ pub struct DbUpdate { asset_object: __sdk::TableUpdate, auth_identity: __sdk::TableUpdate, auth_store_snapshot: __sdk::TableUpdate, + bark_battle_draft_config: __sdk::TableUpdate, + bark_battle_leaderboard_entry: __sdk::TableUpdate, + bark_battle_personal_best_projection: __sdk::TableUpdate, + bark_battle_published_config: __sdk::TableUpdate, + bark_battle_runtime_run: __sdk::TableUpdate, + bark_battle_score_record: __sdk::TableUpdate, + bark_battle_work_stats_projection: __sdk::TableUpdate, battle_state: __sdk::TableUpdate, big_fish_agent_message: __sdk::TableUpdate, big_fish_asset_slot: __sdk::TableUpdate, @@ -2015,6 +2080,33 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { "auth_store_snapshot" => db_update .auth_store_snapshot .append(auth_store_snapshot_table::parse_table_update(table_update)?), + "bark_battle_draft_config" => db_update.bark_battle_draft_config.append( + bark_battle_draft_config_table::parse_table_update(table_update)?, + ), + "bark_battle_leaderboard_entry" => db_update.bark_battle_leaderboard_entry.append( + bark_battle_leaderboard_entry_table::parse_table_update(table_update)?, + ), + "bark_battle_personal_best_projection" => { + db_update.bark_battle_personal_best_projection.append( + bark_battle_personal_best_projection_table::parse_table_update( + table_update, + )?, + ) + } + "bark_battle_published_config" => db_update.bark_battle_published_config.append( + bark_battle_published_config_table::parse_table_update(table_update)?, + ), + "bark_battle_runtime_run" => db_update.bark_battle_runtime_run.append( + bark_battle_runtime_run_table::parse_table_update(table_update)?, + ), + "bark_battle_score_record" => db_update.bark_battle_score_record.append( + bark_battle_score_record_table::parse_table_update(table_update)?, + ), + "bark_battle_work_stats_projection" => { + db_update.bark_battle_work_stats_projection.append( + bark_battle_work_stats_projection_table::parse_table_update(table_update)?, + ) + } "battle_state" => db_update .battle_state .append(battle_state_table::parse_table_update(table_update)?), @@ -2293,6 +2385,48 @@ impl __sdk::DbUpdate for DbUpdate { &self.auth_store_snapshot, ) .with_updates_by_pk(|row| &row.snapshot_id); + diff.bark_battle_draft_config = cache + .apply_diff_to_table::( + "bark_battle_draft_config", + &self.bark_battle_draft_config, + ) + .with_updates_by_pk(|row| &row.draft_id); + diff.bark_battle_leaderboard_entry = cache + .apply_diff_to_table::( + "bark_battle_leaderboard_entry", + &self.bark_battle_leaderboard_entry, + ) + .with_updates_by_pk(|row| &row.leaderboard_entry_id); + diff.bark_battle_personal_best_projection = cache + .apply_diff_to_table::( + "bark_battle_personal_best_projection", + &self.bark_battle_personal_best_projection, + ) + .with_updates_by_pk(|row| &row.personal_best_id); + diff.bark_battle_published_config = cache + .apply_diff_to_table::( + "bark_battle_published_config", + &self.bark_battle_published_config, + ) + .with_updates_by_pk(|row| &row.work_id); + diff.bark_battle_runtime_run = cache + .apply_diff_to_table::( + "bark_battle_runtime_run", + &self.bark_battle_runtime_run, + ) + .with_updates_by_pk(|row| &row.run_id); + diff.bark_battle_score_record = cache + .apply_diff_to_table::( + "bark_battle_score_record", + &self.bark_battle_score_record, + ) + .with_updates_by_pk(|row| &row.score_id); + diff.bark_battle_work_stats_projection = cache + .apply_diff_to_table::( + "bark_battle_work_stats_projection", + &self.bark_battle_work_stats_projection, + ) + .with_updates_by_pk(|row| &row.work_id); diff.battle_state = cache .apply_diff_to_table::("battle_state", &self.battle_state) .with_updates_by_pk(|row| &row.battle_state_id); @@ -2690,6 +2824,27 @@ impl __sdk::DbUpdate for DbUpdate { "auth_store_snapshot" => db_update .auth_store_snapshot .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "bark_battle_draft_config" => db_update + .bark_battle_draft_config + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "bark_battle_leaderboard_entry" => db_update + .bark_battle_leaderboard_entry + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "bark_battle_personal_best_projection" => db_update + .bark_battle_personal_best_projection + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "bark_battle_published_config" => db_update + .bark_battle_published_config + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "bark_battle_runtime_run" => db_update + .bark_battle_runtime_run + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "bark_battle_score_record" => db_update + .bark_battle_score_record + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "bark_battle_work_stats_projection" => db_update + .bark_battle_work_stats_projection + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "battle_state" => db_update .battle_state .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), @@ -2943,6 +3098,27 @@ impl __sdk::DbUpdate for DbUpdate { "auth_store_snapshot" => db_update .auth_store_snapshot .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "bark_battle_draft_config" => db_update + .bark_battle_draft_config + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "bark_battle_leaderboard_entry" => db_update + .bark_battle_leaderboard_entry + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "bark_battle_personal_best_projection" => db_update + .bark_battle_personal_best_projection + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "bark_battle_published_config" => db_update + .bark_battle_published_config + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "bark_battle_runtime_run" => db_update + .bark_battle_runtime_run + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "bark_battle_score_record" => db_update + .bark_battle_score_record + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "bark_battle_work_stats_projection" => db_update + .bark_battle_work_stats_projection + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "battle_state" => db_update .battle_state .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), @@ -3176,6 +3352,15 @@ pub struct AppliedDiff<'r> { asset_object: __sdk::TableAppliedDiff<'r, AssetObject>, auth_identity: __sdk::TableAppliedDiff<'r, AuthIdentity>, auth_store_snapshot: __sdk::TableAppliedDiff<'r, AuthStoreSnapshot>, + bark_battle_draft_config: __sdk::TableAppliedDiff<'r, BarkBattleDraftConfigRow>, + bark_battle_leaderboard_entry: __sdk::TableAppliedDiff<'r, BarkBattleLeaderboardEntryRow>, + bark_battle_personal_best_projection: + __sdk::TableAppliedDiff<'r, BarkBattlePersonalBestProjectionRow>, + bark_battle_published_config: __sdk::TableAppliedDiff<'r, BarkBattlePublishedConfigRow>, + bark_battle_runtime_run: __sdk::TableAppliedDiff<'r, BarkBattleRuntimeRunRow>, + bark_battle_score_record: __sdk::TableAppliedDiff<'r, BarkBattleScoreRecordRow>, + bark_battle_work_stats_projection: + __sdk::TableAppliedDiff<'r, BarkBattleWorkStatsProjectionRow>, battle_state: __sdk::TableAppliedDiff<'r, BattleState>, big_fish_agent_message: __sdk::TableAppliedDiff<'r, BigFishAgentMessage>, big_fish_asset_slot: __sdk::TableAppliedDiff<'r, BigFishAssetSlot>, @@ -3306,6 +3491,41 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { &self.auth_store_snapshot, event, ); + callbacks.invoke_table_row_callbacks::( + "bark_battle_draft_config", + &self.bark_battle_draft_config, + event, + ); + callbacks.invoke_table_row_callbacks::( + "bark_battle_leaderboard_entry", + &self.bark_battle_leaderboard_entry, + event, + ); + callbacks.invoke_table_row_callbacks::( + "bark_battle_personal_best_projection", + &self.bark_battle_personal_best_projection, + event, + ); + callbacks.invoke_table_row_callbacks::( + "bark_battle_published_config", + &self.bark_battle_published_config, + event, + ); + callbacks.invoke_table_row_callbacks::( + "bark_battle_runtime_run", + &self.bark_battle_runtime_run, + event, + ); + callbacks.invoke_table_row_callbacks::( + "bark_battle_score_record", + &self.bark_battle_score_record, + event, + ); + callbacks.invoke_table_row_callbacks::( + "bark_battle_work_stats_projection", + &self.bark_battle_work_stats_projection, + event, + ); callbacks.invoke_table_row_callbacks::( "battle_state", &self.battle_state, @@ -4310,6 +4530,13 @@ impl __sdk::SpacetimeModule for RemoteModule { asset_object_table::register_table(client_cache); auth_identity_table::register_table(client_cache); auth_store_snapshot_table::register_table(client_cache); + bark_battle_draft_config_table::register_table(client_cache); + bark_battle_leaderboard_entry_table::register_table(client_cache); + bark_battle_personal_best_projection_table::register_table(client_cache); + bark_battle_published_config_table::register_table(client_cache); + bark_battle_runtime_run_table::register_table(client_cache); + bark_battle_score_record_table::register_table(client_cache); + bark_battle_work_stats_projection_table::register_table(client_cache); battle_state_table::register_table(client_cache); big_fish_agent_message_table::register_table(client_cache); big_fish_asset_slot_table::register_table(client_cache); @@ -4392,6 +4619,13 @@ impl __sdk::SpacetimeModule for RemoteModule { "asset_object", "auth_identity", "auth_store_snapshot", + "bark_battle_draft_config", + "bark_battle_leaderboard_entry", + "bark_battle_personal_best_projection", + "bark_battle_published_config", + "bark_battle_runtime_run", + "bark_battle_score_record", + "bark_battle_work_stats_projection", "battle_state", "big_fish_agent_message", "big_fish_asset_slot", diff --git a/server-rs/crates/spacetime-client/src/module_bindings/publish_bark_battle_work_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/publish_bark_battle_work_procedure.rs new file mode 100644 index 00000000..87884f54 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/publish_bark_battle_work_procedure.rs @@ -0,0 +1,59 @@ +// 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::bark_battle_procedure_result_type::BarkBattleProcedureResult; +use super::bark_battle_work_publish_input_type::BarkBattleWorkPublishInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct PublishBarkBattleWorkArgs { + pub input: BarkBattleWorkPublishInput, +} + +impl __sdk::InModule for PublishBarkBattleWorkArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `publish_bark_battle_work`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait publish_bark_battle_work { + fn publish_bark_battle_work(&self, input: BarkBattleWorkPublishInput) { + self.publish_bark_battle_work_then(input, |_, _| {}); + } + + fn publish_bark_battle_work_then( + &self, + input: BarkBattleWorkPublishInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl publish_bark_battle_work for super::RemoteProcedures { + fn publish_bark_battle_work_then( + &self, + input: BarkBattleWorkPublishInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, BarkBattleProcedureResult>( + "publish_bark_battle_work", + PublishBarkBattleWorkArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/start_bark_battle_run_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/start_bark_battle_run_procedure.rs new file mode 100644 index 00000000..1fdc1a09 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/start_bark_battle_run_procedure.rs @@ -0,0 +1,59 @@ +// 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::bark_battle_procedure_result_type::BarkBattleProcedureResult; +use super::bark_battle_run_start_input_type::BarkBattleRunStartInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct StartBarkBattleRunArgs { + pub input: BarkBattleRunStartInput, +} + +impl __sdk::InModule for StartBarkBattleRunArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `start_bark_battle_run`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait start_bark_battle_run { + fn start_bark_battle_run(&self, input: BarkBattleRunStartInput) { + self.start_bark_battle_run_then(input, |_, _| {}); + } + + fn start_bark_battle_run_then( + &self, + input: BarkBattleRunStartInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl start_bark_battle_run for super::RemoteProcedures { + fn start_bark_battle_run_then( + &self, + input: BarkBattleRunStartInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, BarkBattleProcedureResult>( + "start_bark_battle_run", + StartBarkBattleRunArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/update_bark_battle_draft_config_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/update_bark_battle_draft_config_procedure.rs new file mode 100644 index 00000000..bd9fc421 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/update_bark_battle_draft_config_procedure.rs @@ -0,0 +1,59 @@ +// 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::bark_battle_draft_config_upsert_input_type::BarkBattleDraftConfigUpsertInput; +use super::bark_battle_procedure_result_type::BarkBattleProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct UpdateBarkBattleDraftConfigArgs { + pub input: BarkBattleDraftConfigUpsertInput, +} + +impl __sdk::InModule for UpdateBarkBattleDraftConfigArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `update_bark_battle_draft_config`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait update_bark_battle_draft_config { + fn update_bark_battle_draft_config(&self, input: BarkBattleDraftConfigUpsertInput) { + self.update_bark_battle_draft_config_then(input, |_, _| {}); + } + + fn update_bark_battle_draft_config_then( + &self, + input: BarkBattleDraftConfigUpsertInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl update_bark_battle_draft_config for super::RemoteProcedures { + fn update_bark_battle_draft_config_then( + &self, + input: BarkBattleDraftConfigUpsertInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, BarkBattleProcedureResult>( + "update_bark_battle_draft_config", + UpdateBarkBattleDraftConfigArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-module/Cargo.toml b/server-rs/crates/spacetime-module/Cargo.toml index 21377ff7..17822404 100644 --- a/server-rs/crates/spacetime-module/Cargo.toml +++ b/server-rs/crates/spacetime-module/Cargo.toml @@ -13,6 +13,7 @@ serde = { workspace = true } serde_json = { workspace = true } module-ai = { workspace = true, features = ["spacetime-types"] } module-assets = { workspace = true, features = ["spacetime-types"] } +module-bark-battle = { workspace = true } module-big-fish = { workspace = true, features = ["spacetime-types"] } module-combat = { workspace = true, features = ["spacetime-types"] } module-inventory = { workspace = true, features = ["spacetime-types"] } @@ -26,6 +27,7 @@ module-runtime = { workspace = true, features = ["spacetime-types"] } module-runtime-item = { workspace = true, features = ["spacetime-types"] } module-square-hole = { workspace = true } module-story = { workspace = true, features = ["spacetime-types"] } +sha2 = { workspace = true } shared-kernel = { workspace = true } spacetimedb = { workspace = true, features = ["unstable"] } spacetimedb-lib = { workspace = true, features = ["serde"] } diff --git a/server-rs/crates/spacetime-module/src/bark_battle/mod.rs b/server-rs/crates/spacetime-module/src/bark_battle/mod.rs new file mode 100644 index 00000000..d4afb89b --- /dev/null +++ b/server-rs/crates/spacetime-module/src/bark_battle/mod.rs @@ -0,0 +1,872 @@ +use crate::*; +use serde::Serialize; +use serde::de::DeserializeOwned; +use sha2::{Digest, Sha256}; + +pub(crate) mod tables; +mod types; + +pub use tables::*; +pub use types::*; + +#[spacetimedb::procedure] +pub fn create_bark_battle_draft( + ctx: &mut ProcedureContext, + input: BarkBattleDraftCreateInput, +) -> BarkBattleProcedureResult { + match ctx.try_with_tx(|tx| create_bark_battle_draft_tx(tx, input.clone())) { + Ok(snapshot) => bark_battle_json_result(&snapshot), + Err(error) => bark_battle_error_result(error), + } +} + +#[spacetimedb::procedure] +pub fn update_bark_battle_draft_config( + ctx: &mut ProcedureContext, + input: BarkBattleDraftConfigUpsertInput, +) -> BarkBattleProcedureResult { + match ctx.try_with_tx(|tx| update_bark_battle_draft_config_tx(tx, input.clone())) { + Ok(snapshot) => bark_battle_json_result(&snapshot), + Err(error) => bark_battle_error_result(error), + } +} + +#[spacetimedb::procedure] +pub fn publish_bark_battle_work( + ctx: &mut ProcedureContext, + input: BarkBattleWorkPublishInput, +) -> BarkBattleProcedureResult { + match ctx.try_with_tx(|tx| publish_bark_battle_work_tx(tx, input.clone())) { + Ok(snapshot) => bark_battle_json_result(&snapshot), + Err(error) => bark_battle_error_result(error), + } +} + +#[spacetimedb::procedure] +pub fn get_bark_battle_runtime_config( + ctx: &mut ProcedureContext, + input: BarkBattleRuntimeConfigGetInput, +) -> BarkBattleProcedureResult { + match ctx.try_with_tx(|tx| get_bark_battle_runtime_config_tx(tx, input.clone())) { + Ok(snapshot) => bark_battle_json_result(&snapshot), + Err(error) => bark_battle_error_result(error), + } +} + +#[spacetimedb::procedure] +pub fn start_bark_battle_run( + ctx: &mut ProcedureContext, + input: BarkBattleRunStartInput, +) -> BarkBattleProcedureResult { + match ctx.try_with_tx(|tx| start_bark_battle_run_tx(tx, input.clone())) { + Ok(snapshot) => bark_battle_json_result(&snapshot), + Err(error) => bark_battle_error_result(error), + } +} + +#[spacetimedb::procedure] +pub fn finish_bark_battle_run( + ctx: &mut ProcedureContext, + input: BarkBattleRunFinishInput, +) -> BarkBattleProcedureResult { + match ctx.try_with_tx(|tx| finish_bark_battle_run_tx(tx, input.clone())) { + Ok(snapshot) => bark_battle_json_result(&snapshot), + Err(error) => bark_battle_error_result(error), + } +} + +#[spacetimedb::procedure] +pub fn get_bark_battle_run( + ctx: &mut ProcedureContext, + input: BarkBattleRunGetInput, +) -> BarkBattleProcedureResult { + match ctx.try_with_tx(|tx| get_bark_battle_run_tx(tx, input.clone())) { + Ok(snapshot) => bark_battle_json_result(&snapshot), + Err(error) => bark_battle_error_result(error), + } +} + +fn create_bark_battle_draft_tx( + ctx: &ReducerContext, + input: BarkBattleDraftCreateInput, +) -> Result { + require_non_empty(&input.draft_id, "bark_battle draft_id")?; + require_non_empty(&input.owner_user_id, "bark_battle owner_user_id")?; + require_non_empty(&input.work_id, "bark_battle work_id")?; + if ctx + .db + .bark_battle_draft_config() + .draft_id() + .find(&input.draft_id) + .is_some() + { + return Err("bark_battle_draft_config.draft_id 已存在".to_string()); + } + + let now = Timestamp::from_micros_since_unix_epoch(input.created_at_micros); + let config = BarkBattleEditorConfigSnapshot { + title: normalize_title(input.title.as_deref())?, + description: normalize_optional_text(input.description.as_deref()), + theme_preset: normalize_required_preset(&input.theme_preset, "theme_preset")?, + player_dog_skin_preset: normalize_required_preset( + &input.player_dog_skin_preset, + "player_dog_skin_preset", + )?, + opponent_dog_skin_preset: normalize_required_preset( + &input.opponent_dog_skin_preset, + "opponent_dog_skin_preset", + )?, + difficulty_preset: normalize_difficulty(input.difficulty_preset.as_deref())?, + leaderboard_enabled: input.leaderboard_enabled.unwrap_or(true), + }; + let row = BarkBattleDraftConfigRow { + draft_id: input.draft_id.clone(), + owner_user_id: input.owner_user_id.clone(), + work_id: input.work_id.clone(), + config_version: 1, + ruleset_version: BARK_BATTLE_DEFAULT_RULESET_VERSION.to_string(), + difficulty_preset: config.difficulty_preset.clone(), + leaderboard_enabled: config.leaderboard_enabled, + config_json: to_json_string(&config), + editor_state_json: normalize_json_string( + input.editor_state_json.as_deref(), + "editor_state_json", + )?, + created_at: now, + updated_at: now, + }; + ctx.db.bark_battle_draft_config().insert(row.clone()); + Ok(draft_snapshot(&row)) +} + +fn update_bark_battle_draft_config_tx( + ctx: &ReducerContext, + input: BarkBattleDraftConfigUpsertInput, +) -> Result { + require_non_empty(&input.draft_id, "bark_battle draft_id")?; + require_non_empty(&input.owner_user_id, "bark_battle owner_user_id")?; + require_non_empty(&input.work_id, "bark_battle work_id")?; + let editor_config = parse_editor_config(&input.config_json)?; + validate_editor_config_snapshot(&editor_config)?; + if editor_config.difficulty_preset != input.difficulty_preset + || editor_config.leaderboard_enabled != input.leaderboard_enabled + { + return Err("bark_battle config_json 与行字段不一致".to_string()); + } + let updated_at = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros); + let existing = ctx + .db + .bark_battle_draft_config() + .draft_id() + .find(&input.draft_id) + .ok_or_else(|| "bark_battle_draft_config.draft_id 不存在".to_string())?; + if existing.owner_user_id != input.owner_user_id || existing.work_id != input.work_id { + return Err("bark_battle draft owner/work 不匹配".to_string()); + } + if input.config_version <= existing.config_version { + return Err("bark_battle draft config_version 必须递增".to_string()); + } + let mut row = existing; + row.config_version = input.config_version; + row.ruleset_version = normalize_ruleset_version(&input.ruleset_version)?; + row.difficulty_preset = normalize_difficulty(Some(&input.difficulty_preset))?; + row.leaderboard_enabled = input.leaderboard_enabled; + row.config_json = input.config_json; + row.updated_at = updated_at; + ctx.db + .bark_battle_draft_config() + .draft_id() + .update(row.clone()); + Ok(draft_snapshot(&row)) +} + +fn publish_bark_battle_work_tx( + ctx: &ReducerContext, + input: BarkBattleWorkPublishInput, +) -> Result { + require_non_empty(&input.draft_id, "bark_battle draft_id")?; + require_non_empty(&input.owner_user_id, "bark_battle owner_user_id")?; + require_non_empty(&input.work_id, "bark_battle work_id")?; + let published_at = Timestamp::from_micros_since_unix_epoch(input.published_at_micros); + let draft = ctx + .db + .bark_battle_draft_config() + .draft_id() + .find(&input.draft_id) + .ok_or_else(|| "bark_battle draft 不存在".to_string())?; + if draft.owner_user_id != input.owner_user_id || draft.work_id != input.work_id { + return Err("bark_battle draft owner/work 不匹配".to_string()); + } + let published = BarkBattlePublishedConfigRow { + work_id: draft.work_id.clone(), + owner_user_id: draft.owner_user_id.clone(), + source_draft_id: Some(draft.draft_id.clone()), + config_version: draft.config_version, + ruleset_version: normalize_ruleset_version(&draft.ruleset_version)?, + difficulty_preset: normalize_difficulty(Some(&draft.difficulty_preset))?, + leaderboard_enabled: draft.leaderboard_enabled, + config_json: draft.config_json.clone(), + published_snapshot_json: match input.published_snapshot_json.as_deref() { + Some(value) => normalize_json_string(Some(value), "published_snapshot_json")?, + None => draft.config_json.clone(), + }, + created_at: published_at, + updated_at: published_at, + published_at, + }; + let mut published = published; + match ctx + .db + .bark_battle_published_config() + .work_id() + .find(&published.work_id) + { + Some(existing) => { + published.created_at = existing.created_at; + ctx.db + .bark_battle_published_config() + .work_id() + .update(published.clone()); + } + None => { + ctx.db + .bark_battle_published_config() + .insert(published.clone()); + } + } + Ok(runtime_config_snapshot(&published)) +} + +fn get_bark_battle_runtime_config_tx( + ctx: &ReducerContext, + input: BarkBattleRuntimeConfigGetInput, +) -> Result { + require_non_empty(&input.work_id, "bark_battle work_id")?; + let row = ctx + .db + .bark_battle_published_config() + .work_id() + .find(&input.work_id) + .ok_or_else(|| "bark_battle published config 不存在".to_string())?; + if let Some(owner_user_id) = input.owner_user_id.as_deref() { + if !owner_user_id.trim().is_empty() && row.owner_user_id != owner_user_id.trim() { + return Err("bark_battle runtime config owner 不匹配".to_string()); + } + } + Ok(runtime_config_snapshot(&row)) +} + +fn start_bark_battle_run_tx( + ctx: &ReducerContext, + input: BarkBattleRunStartInput, +) -> Result { + require_non_empty(&input.run_id, "bark_battle run_id")?; + require_non_empty(&input.run_token, "bark_battle run_token")?; + require_non_empty(&input.owner_user_id, "bark_battle owner_user_id")?; + require_non_empty(&input.work_id, "bark_battle work_id")?; + let published = ctx + .db + .bark_battle_published_config() + .work_id() + .find(&input.work_id) + .ok_or_else(|| "bark_battle published config 不存在".to_string())?; + if published.config_version != input.config_version + || published.ruleset_version != input.ruleset_version + || published.difficulty_preset != input.difficulty_preset + { + return Err("bark_battle run config/ruleset/difficulty 不匹配".to_string()); + } + if ctx + .db + .bark_battle_runtime_run() + .run_id() + .find(&input.run_id) + .is_some() + { + return Err("bark_battle run_id 已存在".to_string()); + } + let started_at = ctx.timestamp; + let row = BarkBattleRuntimeRunRow { + run_id: input.run_id, + run_token_hash: hash_run_token(&input.run_token), + owner_user_id: input.owner_user_id, + work_id: input.work_id, + config_version: input.config_version, + ruleset_version: input.ruleset_version, + difficulty_preset: input.difficulty_preset, + leaderboard_enabled: published.leaderboard_enabled, + status: BARK_BATTLE_RUN_RUNNING.to_string(), + client_started_at_micros: input.client_started_at_micros, + server_started_at: started_at, + client_finished_at_micros: None, + server_finished_at: None, + metrics_json: "{}".to_string(), + server_result: None, + validation_status: BARK_BATTLE_VALIDATION_PENDING.to_string(), + anti_cheat_flags_json: "[]".to_string(), + leaderboard_score: None, + score_id: None, + created_at: started_at, + updated_at: started_at, + }; + ctx.db.bark_battle_runtime_run().insert(row.clone()); + upsert_initial_work_stats(ctx, &row); + Ok(run_snapshot(&row)) +} + +fn finish_bark_battle_run_tx( + ctx: &ReducerContext, + input: BarkBattleRunFinishInput, +) -> Result { + require_non_empty(&input.run_id, "bark_battle run_id")?; + require_non_empty(&input.run_token, "bark_battle run_token")?; + let mut run = ctx + .db + .bark_battle_runtime_run() + .run_id() + .find(&input.run_id) + .ok_or_else(|| "bark_battle run 不存在".to_string())?; + if input.server_finished_at_micros > 0 + && input.server_finished_at_micros < run.server_started_at.to_micros_since_unix_epoch() + { + return Err("bark_battle server_finished_at 早于 run start".to_string()); + } + if ctx + .timestamp + .to_micros_since_unix_epoch() + .saturating_sub(run.server_started_at.to_micros_since_unix_epoch()) + > 10 * 60 * 1_000_000 + { + return Err("bark_battle run 已过期".to_string()); + } + if run.run_token_hash != hash_run_token(&input.run_token) { + return Err("bark_battle run_token 不匹配".to_string()); + } + if run.status != BARK_BATTLE_RUN_RUNNING { + return Err("bark_battle run 已结束".to_string()); + } + if run.owner_user_id != input.owner_user_id + || run.work_id != input.work_id + || run.config_version != input.config_version + || run.ruleset_version != input.ruleset_version + || run.difficulty_preset != input.difficulty_preset + { + return Err("bark_battle finish identity/config 不匹配".to_string()); + } + validate_json::(&input.metrics_json, "metrics_json")?; + validate_json::(&input.derived_metrics_json, "derived_metrics_json")?; + + let difficulty = parse_domain_difficulty(&input.difficulty_preset)?; + let ruleset = module_bark_battle::BarkBattleRuleset::for_difficulty(difficulty); + let finished_at = ctx.timestamp; + let metrics = module_bark_battle::BarkBattleFinishMetrics { + duration_ms: input.duration_ms, + trigger_count: input.trigger_count, + max_volume: millis_to_unit(input.max_volume_millis), + average_volume: millis_to_unit(input.average_volume_millis), + final_energy: millis_to_energy(input.final_energy_millis), + max_combo: input.max_combo, + finished_at_micros: finished_at.to_micros_since_unix_epoch(), + }; + let validation = module_bark_battle::validate_finish_metrics(&ruleset, &metrics); + let result = module_bark_battle::adjudicate_result( + &ruleset, + metrics.final_energy, + millis_to_energy(input.opponent_final_energy_millis), + ); + let leaderboard = if run.leaderboard_enabled { + module_bark_battle::compute_leaderboard_score(&ruleset, &metrics, &validation, result) + } else { + None + }; + let leaderboard_score = leaderboard.map(compose_leaderboard_score); + let score_id = format!("score-{}", input.run_id); + let validation_status = validation_status_to_string(validation.decision); + let server_result = battle_result_to_string(result); + let flags_json = to_json_string(&validation.anti_cheat_flags); + + ctx.db + .bark_battle_score_record() + .insert(BarkBattleScoreRecordRow { + score_id: score_id.clone(), + owner_user_id: run.owner_user_id.clone(), + work_id: run.work_id.clone(), + run_id: run.run_id.clone(), + config_version: run.config_version, + ruleset_version: run.ruleset_version.clone(), + difficulty_preset: run.difficulty_preset.clone(), + leaderboard_enabled: run.leaderboard_enabled, + metrics_json: input.metrics_json.clone(), + derived_metrics_json: input.derived_metrics_json.clone(), + server_result: server_result.clone(), + validation_status: validation_status.clone(), + anti_cheat_flags_json: flags_json.clone(), + leaderboard_score, + recorded_at: finished_at, + }); + + if let Some(score) = leaderboard_score { + ctx.db + .bark_battle_leaderboard_entry() + .insert(BarkBattleLeaderboardEntryRow { + leaderboard_entry_id: format!("leaderboard-{}", input.run_id), + work_id: run.work_id.clone(), + owner_user_id: run.owner_user_id.clone(), + run_id: run.run_id.clone(), + score_id: score_id.clone(), + leaderboard_score: score, + final_energy: metrics.final_energy, + trigger_count: metrics.trigger_count, + max_volume: metrics.max_volume, + duration_closeness_ms: input.duration_ms.abs_diff(ruleset.standard_duration_ms), + finished_at_micros: finished_at.to_micros_since_unix_epoch(), + created_at: finished_at, + updated_at: finished_at, + }); + } + + run.status = BARK_BATTLE_RUN_FINISHED.to_string(); + run.client_finished_at_micros = Some(input.client_finished_at_micros); + run.server_finished_at = Some(finished_at); + run.metrics_json = input.metrics_json; + run.server_result = Some(server_result.clone()); + run.validation_status = validation_status.clone(); + run.anti_cheat_flags_json = flags_json; + run.leaderboard_score = leaderboard_score; + run.score_id = Some(score_id.clone()); + run.updated_at = finished_at; + ctx.db + .bark_battle_runtime_run() + .run_id() + .update(run.clone()); + upsert_finished_projections( + ctx, + &run, + &score_id, + leaderboard_score, + metrics.final_energy, + metrics.trigger_count, + metrics.max_volume, + input.duration_ms.abs_diff(ruleset.standard_duration_ms), + &server_result, + &validation_status, + finished_at.to_micros_since_unix_epoch(), + finished_at, + ); + Ok(run_snapshot(&run)) +} + +fn get_bark_battle_run_tx( + ctx: &ReducerContext, + input: BarkBattleRunGetInput, +) -> Result { + let row = ctx + .db + .bark_battle_runtime_run() + .run_id() + .find(&input.run_id) + .ok_or_else(|| "bark_battle run 不存在".to_string())?; + if row.owner_user_id != input.owner_user_id { + return Err("bark_battle run owner 不匹配".to_string()); + } + Ok(run_snapshot(&row)) +} + +fn draft_snapshot(row: &BarkBattleDraftConfigRow) -> BarkBattleDraftConfigSnapshot { + BarkBattleDraftConfigSnapshot { + draft_id: row.draft_id.clone(), + owner_user_id: row.owner_user_id.clone(), + work_id: row.work_id.clone(), + config_version: row.config_version, + ruleset_version: row.ruleset_version.clone(), + difficulty_preset: row.difficulty_preset.clone(), + leaderboard_enabled: row.leaderboard_enabled, + config_json: row.config_json.clone(), + editor_state_json: row.editor_state_json.clone(), + created_at_micros: row.created_at.to_micros_since_unix_epoch(), + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + } +} + +fn runtime_config_snapshot(row: &BarkBattlePublishedConfigRow) -> BarkBattleRuntimeConfigSnapshot { + BarkBattleRuntimeConfigSnapshot { + work_id: row.work_id.clone(), + owner_user_id: row.owner_user_id.clone(), + source_draft_id: row.source_draft_id.clone(), + config_version: row.config_version, + ruleset_version: row.ruleset_version.clone(), + difficulty_preset: row.difficulty_preset.clone(), + leaderboard_enabled: row.leaderboard_enabled, + config_json: row.config_json.clone(), + published_snapshot_json: row.published_snapshot_json.clone(), + published_at_micros: row.published_at.to_micros_since_unix_epoch(), + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + } +} + +fn hash_run_token(token: &str) -> String { + let digest = Sha256::digest(token.as_bytes()); + digest.iter().map(|byte| format!("{byte:02x}")).collect() +} + +fn normalize_json_string(value: Option<&str>, field_name: &str) -> Result { + let json = value.unwrap_or("{}").trim(); + validate_json::(json, field_name)?; + Ok(json.to_string()) +} + +fn require_non_empty(value: &str, label: &str) -> Result<(), String> { + if value.trim().is_empty() { + Err(format!("{label} 不能为空")) + } else { + Ok(()) + } +} + +fn validate_editor_config_snapshot(config: &BarkBattleEditorConfigSnapshot) -> Result<(), String> { + normalize_title(Some(&config.title))?; + normalize_required_preset(&config.theme_preset, "theme_preset")?; + normalize_required_preset(&config.player_dog_skin_preset, "player_dog_skin_preset")?; + normalize_required_preset(&config.opponent_dog_skin_preset, "opponent_dog_skin_preset")?; + normalize_difficulty(Some(&config.difficulty_preset))?; + Ok(()) +} + +fn normalize_title(value: Option<&str>) -> Result { + let title = value.unwrap_or("汪汪声浪挑战").trim(); + if title.is_empty() { + return Err("bark_battle title 不能为空".to_string()); + } + if title.chars().count() > 40 { + return Err("bark_battle title 不能超过 40 个字符".to_string()); + } + Ok(title.to_string()) +} + +fn normalize_optional_text(value: Option<&str>) -> String { + value.unwrap_or_default().trim().chars().take(120).collect() +} + +fn normalize_required_preset(value: &str, field_name: &str) -> Result { + let preset = value.trim(); + if preset.is_empty() { + return Err(format!("bark_battle {field_name} 不能为空")); + } + Ok(preset.to_string()) +} + +fn normalize_ruleset_version(value: &str) -> Result { + let ruleset = value.trim(); + if ruleset != BARK_BATTLE_DEFAULT_RULESET_VERSION { + return Err("bark_battle ruleset_version 不支持".to_string()); + } + Ok(ruleset.to_string()) +} + +fn normalize_difficulty(value: Option<&str>) -> Result { + let difficulty = value.unwrap_or(BARK_BATTLE_DIFFICULTY_NORMAL).trim(); + match difficulty { + BARK_BATTLE_DIFFICULTY_EASY + | BARK_BATTLE_DIFFICULTY_NORMAL + | BARK_BATTLE_DIFFICULTY_HARD => Ok(difficulty.to_string()), + _ => Err("bark_battle difficulty_preset 不支持".to_string()), + } +} + +fn parse_editor_config(value: &str) -> Result { + serde_json::from_str::(value) + .map_err(|error| format!("bark_battle config_json JSON 无效: {error}")) +} + +fn validate_json(value: &str, field_name: &str) -> Result<(), String> { + serde_json::from_str::(value) + .map(|_| ()) + .map_err(|error| format!("bark_battle {field_name} JSON 无效: {error}")) +} + +fn bark_battle_json_result(value: &T) -> BarkBattleProcedureResult { + BarkBattleProcedureResult { + ok: true, + row_json: Some(to_json_string(value)), + error_message: None, + } +} + +fn bark_battle_error_result(error: String) -> BarkBattleProcedureResult { + BarkBattleProcedureResult { + ok: false, + row_json: None, + error_message: Some(error), + } +} + +fn to_json_string(value: &T) -> String { + serde_json::to_string(value).expect("serialize bark battle snapshot") +} + +fn run_snapshot(row: &BarkBattleRuntimeRunRow) -> BarkBattleRunSnapshot { + BarkBattleRunSnapshot { + run_id: row.run_id.clone(), + owner_user_id: row.owner_user_id.clone(), + work_id: row.work_id.clone(), + config_version: row.config_version, + ruleset_version: row.ruleset_version.clone(), + difficulty_preset: row.difficulty_preset.clone(), + leaderboard_enabled: row.leaderboard_enabled, + status: row.status.clone(), + client_started_at_micros: row.client_started_at_micros, + server_started_at_micros: row.server_started_at.to_micros_since_unix_epoch(), + client_finished_at_micros: row.client_finished_at_micros, + server_finished_at_micros: row + .server_finished_at + .map(|t| t.to_micros_since_unix_epoch()), + metrics_json: row.metrics_json.clone(), + server_result: row.server_result.clone(), + validation_status: row.validation_status.clone(), + anti_cheat_flags_json: row.anti_cheat_flags_json.clone(), + leaderboard_score: row.leaderboard_score, + score_id: row.score_id.clone(), + } +} + +fn upsert_initial_work_stats(ctx: &ReducerContext, run: &BarkBattleRuntimeRunRow) { + let now = run.created_at; + match ctx + .db + .bark_battle_work_stats_projection() + .work_id() + .find(&run.work_id) + { + Some(mut stats) => { + stats.play_count += 1; + stats.updated_at = now; + ctx.db + .bark_battle_work_stats_projection() + .work_id() + .update(stats); + } + None => { + ctx.db + .bark_battle_work_stats_projection() + .insert(BarkBattleWorkStatsProjectionRow { + work_id: run.work_id.clone(), + owner_user_id: run.owner_user_id.clone(), + play_count: 1, + finished_count: 0, + accepted_score_count: 0, + leaderboard_entry_count: 0, + best_leaderboard_score: None, + best_score_id: None, + best_run_id: None, + average_final_energy: 0.0, + average_trigger_count: 0.0, + last_finished_at_micros: None, + stats_json: "{}".to_string(), + updated_at: now, + }); + } + } +} + +#[allow(clippy::too_many_arguments)] +fn upsert_finished_projections( + ctx: &ReducerContext, + run: &BarkBattleRuntimeRunRow, + score_id: &str, + leaderboard_score: Option, + final_energy: f32, + trigger_count: u64, + max_volume: f32, + duration_closeness_ms: u64, + server_result: &str, + validation_status: &str, + finished_at_micros: i64, + updated_at: Timestamp, +) { + let mut stats = ctx + .db + .bark_battle_work_stats_projection() + .work_id() + .find(&run.work_id) + .unwrap_or_else(|| BarkBattleWorkStatsProjectionRow { + work_id: run.work_id.clone(), + owner_user_id: run.owner_user_id.clone(), + play_count: 0, + finished_count: 0, + accepted_score_count: 0, + leaderboard_entry_count: 0, + best_leaderboard_score: None, + best_score_id: None, + best_run_id: None, + average_final_energy: 0.0, + average_trigger_count: 0.0, + last_finished_at_micros: None, + stats_json: "{}".to_string(), + updated_at, + }); + let previous_finished = stats.finished_count as f32; + stats.finished_count += 1; + if validation_status != BARK_BATTLE_VALIDATION_REJECTED { + stats.accepted_score_count += 1; + } + if leaderboard_score.is_some() { + stats.leaderboard_entry_count += 1; + } + stats.average_final_energy = ((stats.average_final_energy * previous_finished) + final_energy) + / stats.finished_count as f32; + stats.average_trigger_count = ((stats.average_trigger_count * previous_finished) + + trigger_count as f32) + / stats.finished_count as f32; + if leaderboard_score > stats.best_leaderboard_score { + stats.best_leaderboard_score = leaderboard_score; + stats.best_score_id = Some(score_id.to_string()); + stats.best_run_id = Some(run.run_id.clone()); + } + stats.last_finished_at_micros = Some(finished_at_micros); + stats.updated_at = updated_at; + if ctx + .db + .bark_battle_work_stats_projection() + .work_id() + .find(&run.work_id) + .is_some() + { + ctx.db + .bark_battle_work_stats_projection() + .work_id() + .update(stats); + } else { + ctx.db.bark_battle_work_stats_projection().insert(stats); + } + + let personal_best_id = format!("{}:{}", run.owner_user_id, run.work_id); + let should_update_best = validation_status != BARK_BATTLE_VALIDATION_REJECTED + && ctx + .db + .bark_battle_personal_best_projection() + .personal_best_id() + .find(&personal_best_id) + .map(|best| { + leaderboard_score > best.leaderboard_score || final_energy > best.final_energy + }) + .unwrap_or(true); + if should_update_best { + let row = BarkBattlePersonalBestProjectionRow { + personal_best_id: personal_best_id.clone(), + owner_user_id: run.owner_user_id.clone(), + work_id: run.work_id.clone(), + run_id: run.run_id.clone(), + score_id: score_id.to_string(), + leaderboard_entry_id: leaderboard_score.map(|_| format!("leaderboard-{}", run.run_id)), + leaderboard_score, + final_energy, + trigger_count, + max_volume, + duration_closeness_ms, + server_result: server_result.to_string(), + validation_status: validation_status.to_string(), + finished_at_micros, + summary_json: "{}".to_string(), + updated_at, + }; + if ctx + .db + .bark_battle_personal_best_projection() + .personal_best_id() + .find(&personal_best_id) + .is_some() + { + ctx.db + .bark_battle_personal_best_projection() + .personal_best_id() + .update(row); + } else { + ctx.db.bark_battle_personal_best_projection().insert(row); + } + } +} + +fn parse_domain_difficulty(value: &str) -> Result { + match value { + BARK_BATTLE_DIFFICULTY_EASY => Ok(module_bark_battle::DifficultyPreset::Easy), + BARK_BATTLE_DIFFICULTY_NORMAL => Ok(module_bark_battle::DifficultyPreset::Normal), + BARK_BATTLE_DIFFICULTY_HARD => Ok(module_bark_battle::DifficultyPreset::Hard), + _ => Err("bark_battle difficulty_preset 不支持".to_string()), + } +} + +fn millis_to_unit(value: u32) -> f32 { + value as f32 / 1_000.0 +} + +fn millis_to_energy(value: u32) -> f32 { + value as f32 / 1_000.0 +} + +fn validation_status_to_string(decision: module_bark_battle::FinishValidationDecision) -> String { + match decision { + module_bark_battle::FinishValidationDecision::Accepted => BARK_BATTLE_VALIDATION_ACCEPTED, + module_bark_battle::FinishValidationDecision::AcceptedWithFlags => { + BARK_BATTLE_VALIDATION_ACCEPTED_WITH_FLAGS + } + module_bark_battle::FinishValidationDecision::Rejected => BARK_BATTLE_VALIDATION_REJECTED, + } + .to_string() +} + +fn battle_result_to_string(result: module_bark_battle::BattleResult) -> String { + match result { + module_bark_battle::BattleResult::PlayerWin => "player_win", + module_bark_battle::BattleResult::OpponentWin => "opponent_win", + module_bark_battle::BattleResult::Draw => "draw", + } + .to_string() +} + +fn compose_leaderboard_score(score: module_bark_battle::BarkBattleLeaderboardScore) -> u64 { + u64::from(score.final_energy_millis) * 10_000_000 + + score.trigger_count.min(9_999) + + u64::from(score.max_volume_millis).min(999) * 10_000 + + (10_000_u64.saturating_sub(score.duration_closeness_ms.min(10_000))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bark_battle_types_are_constructible() { + let input = BarkBattleDraftConfigUpsertInput { + draft_id: "draft-1".to_string(), + owner_user_id: "user-1".to_string(), + work_id: "work-1".to_string(), + config_version: 1, + ruleset_version: BARK_BATTLE_DEFAULT_RULESET_VERSION.to_string(), + difficulty_preset: BARK_BATTLE_DIFFICULTY_NORMAL.to_string(), + leaderboard_enabled: true, + config_json: "{}".to_string(), + updated_at_micros: 1_700_000, + }; + + let result = BarkBattleProcedureResult { + ok: true, + row_json: Some(input.config_json.clone()), + error_message: None, + }; + + assert_eq!(input.draft_id, "draft-1"); + assert_eq!(input.ruleset_version, BARK_BATTLE_DEFAULT_RULESET_VERSION); + assert!(result.ok); + } + + #[test] + fn validates_light_editor_config_before_publish() { + assert_eq!( + normalize_difficulty(Some(BARK_BATTLE_DIFFICULTY_HARD)).expect("difficulty"), + BARK_BATTLE_DIFFICULTY_HARD + ); + assert!(normalize_difficulty(Some("insane")).is_err()); + assert!(normalize_title(Some(" 标题 ")).is_ok()); + assert!(normalize_title(Some(" ")).is_err()); + } +} diff --git a/server-rs/crates/spacetime-module/src/bark_battle/tables.rs b/server-rs/crates/spacetime-module/src/bark_battle/tables.rs new file mode 100644 index 00000000..9b436e6a --- /dev/null +++ b/server-rs/crates/spacetime-module/src/bark_battle/tables.rs @@ -0,0 +1,172 @@ +use crate::*; + +#[spacetimedb::table( + accessor = bark_battle_draft_config, + index(accessor = by_bark_battle_draft_owner_user_id, btree(columns = [owner_user_id])), + index(accessor = by_bark_battle_draft_work_id, btree(columns = [work_id])) +)] +#[derive(Clone)] +pub struct BarkBattleDraftConfigRow { + #[primary_key] + pub(crate) draft_id: String, + pub(crate) owner_user_id: String, + pub(crate) work_id: String, + pub(crate) config_version: u64, + pub(crate) ruleset_version: String, + pub(crate) difficulty_preset: String, + pub(crate) leaderboard_enabled: bool, + pub(crate) config_json: String, + pub(crate) editor_state_json: String, + pub(crate) created_at: Timestamp, + pub(crate) updated_at: Timestamp, +} + +#[spacetimedb::table( + accessor = bark_battle_published_config, + index(accessor = by_bark_battle_published_owner_user_id, btree(columns = [owner_user_id])) +)] +#[derive(Clone)] +pub struct BarkBattlePublishedConfigRow { + #[primary_key] + pub(crate) work_id: String, + pub(crate) owner_user_id: String, + pub(crate) source_draft_id: Option, + pub(crate) config_version: u64, + pub(crate) ruleset_version: String, + pub(crate) difficulty_preset: String, + pub(crate) leaderboard_enabled: bool, + pub(crate) config_json: String, + pub(crate) published_snapshot_json: String, + pub(crate) created_at: Timestamp, + pub(crate) updated_at: Timestamp, + pub(crate) published_at: Timestamp, +} + +#[spacetimedb::table( + accessor = bark_battle_runtime_run, + index(accessor = by_bark_battle_run_owner_user_id, btree(columns = [owner_user_id])), + index(accessor = by_bark_battle_run_work_id, btree(columns = [work_id])) +)] +#[derive(Clone)] +pub struct BarkBattleRuntimeRunRow { + #[primary_key] + pub(crate) run_id: String, + pub(crate) run_token_hash: String, + pub(crate) owner_user_id: String, + pub(crate) work_id: String, + pub(crate) config_version: u64, + pub(crate) ruleset_version: String, + pub(crate) difficulty_preset: String, + pub(crate) leaderboard_enabled: bool, + pub(crate) status: String, + pub(crate) client_started_at_micros: i64, + pub(crate) server_started_at: Timestamp, + pub(crate) client_finished_at_micros: Option, + pub(crate) server_finished_at: Option, + pub(crate) metrics_json: String, + pub(crate) server_result: Option, + pub(crate) validation_status: String, + pub(crate) anti_cheat_flags_json: String, + pub(crate) leaderboard_score: Option, + pub(crate) score_id: Option, + pub(crate) created_at: Timestamp, + pub(crate) updated_at: Timestamp, +} + +#[spacetimedb::table( + accessor = bark_battle_score_record, + index(accessor = by_bark_battle_score_owner_user_id, btree(columns = [owner_user_id])), + index(accessor = by_bark_battle_score_work_id, btree(columns = [work_id])), + index(accessor = by_bark_battle_score_run_id, btree(columns = [run_id])) +)] +#[derive(Clone)] +pub struct BarkBattleScoreRecordRow { + #[primary_key] + pub(crate) score_id: String, + pub(crate) owner_user_id: String, + pub(crate) work_id: String, + pub(crate) run_id: String, + pub(crate) config_version: u64, + pub(crate) ruleset_version: String, + pub(crate) difficulty_preset: String, + pub(crate) leaderboard_enabled: bool, + pub(crate) metrics_json: String, + pub(crate) derived_metrics_json: String, + pub(crate) server_result: String, + pub(crate) validation_status: String, + pub(crate) anti_cheat_flags_json: String, + pub(crate) leaderboard_score: Option, + pub(crate) recorded_at: Timestamp, +} + +#[spacetimedb::table( + accessor = bark_battle_leaderboard_entry, + index(accessor = by_bark_battle_leaderboard_work_score, btree(columns = [work_id, leaderboard_score])), + index(accessor = by_bark_battle_leaderboard_owner_work, btree(columns = [owner_user_id, work_id])) +)] +#[derive(Clone)] +pub struct BarkBattleLeaderboardEntryRow { + #[primary_key] + pub(crate) leaderboard_entry_id: String, + pub(crate) work_id: String, + pub(crate) owner_user_id: String, + pub(crate) run_id: String, + pub(crate) score_id: String, + pub(crate) leaderboard_score: u64, + pub(crate) final_energy: f32, + pub(crate) trigger_count: u64, + pub(crate) max_volume: f32, + pub(crate) duration_closeness_ms: u64, + pub(crate) finished_at_micros: i64, + pub(crate) created_at: Timestamp, + pub(crate) updated_at: Timestamp, +} + +#[spacetimedb::table( + accessor = bark_battle_work_stats_projection, + index(accessor = by_bark_battle_work_stats_owner_user_id, btree(columns = [owner_user_id])) +)] +#[derive(Clone)] +pub struct BarkBattleWorkStatsProjectionRow { + #[primary_key] + pub(crate) work_id: String, + pub(crate) owner_user_id: String, + pub(crate) play_count: u64, + pub(crate) finished_count: u64, + pub(crate) accepted_score_count: u64, + pub(crate) leaderboard_entry_count: u64, + pub(crate) best_leaderboard_score: Option, + pub(crate) best_score_id: Option, + pub(crate) best_run_id: Option, + pub(crate) average_final_energy: f32, + pub(crate) average_trigger_count: f32, + pub(crate) last_finished_at_micros: Option, + pub(crate) stats_json: String, + pub(crate) updated_at: Timestamp, +} + +#[spacetimedb::table( + accessor = bark_battle_personal_best_projection, + index(accessor = by_bark_battle_personal_best_work_id, btree(columns = [work_id])), + index(accessor = by_bark_battle_personal_best_owner_work, btree(columns = [owner_user_id, work_id])) +)] +#[derive(Clone)] +pub struct BarkBattlePersonalBestProjectionRow { + #[primary_key] + pub(crate) personal_best_id: String, + pub(crate) owner_user_id: String, + pub(crate) work_id: String, + pub(crate) run_id: String, + pub(crate) score_id: String, + pub(crate) leaderboard_entry_id: Option, + pub(crate) leaderboard_score: Option, + pub(crate) final_energy: f32, + pub(crate) trigger_count: u64, + pub(crate) max_volume: f32, + pub(crate) duration_closeness_ms: u64, + pub(crate) server_result: String, + pub(crate) validation_status: String, + pub(crate) finished_at_micros: i64, + pub(crate) summary_json: String, + pub(crate) updated_at: Timestamp, +} diff --git a/server-rs/crates/spacetime-module/src/bark_battle/types.rs b/server-rs/crates/spacetime-module/src/bark_battle/types.rs new file mode 100644 index 00000000..e26a2747 --- /dev/null +++ b/server-rs/crates/spacetime-module/src/bark_battle/types.rs @@ -0,0 +1,177 @@ +use crate::*; +use serde::{Deserialize, Serialize}; + +pub const BARK_BATTLE_DEFAULT_RULESET_VERSION: &str = + module_bark_battle::BARK_BATTLE_RULESET_VERSION_V1; +pub const BARK_BATTLE_DIFFICULTY_EASY: &str = "easy"; +pub const BARK_BATTLE_DIFFICULTY_NORMAL: &str = "normal"; +pub const BARK_BATTLE_DIFFICULTY_HARD: &str = "hard"; + +pub const BARK_BATTLE_RUN_PENDING: &str = "Pending"; +pub const BARK_BATTLE_RUN_RUNNING: &str = "Running"; +pub const BARK_BATTLE_RUN_FINISHED: &str = "Finished"; +pub const BARK_BATTLE_RUN_ABORTED: &str = "Aborted"; + +pub const BARK_BATTLE_VALIDATION_PENDING: &str = "pending"; +pub const BARK_BATTLE_VALIDATION_ACCEPTED: &str = "accepted"; +pub const BARK_BATTLE_VALIDATION_ACCEPTED_WITH_FLAGS: &str = "accepted_with_flags"; +pub const BARK_BATTLE_VALIDATION_REJECTED: &str = "rejected"; + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct BarkBattleDraftCreateInput { + pub draft_id: String, + pub owner_user_id: String, + pub work_id: String, + pub title: Option, + pub description: Option, + pub theme_preset: String, + pub player_dog_skin_preset: String, + pub opponent_dog_skin_preset: String, + pub difficulty_preset: Option, + pub leaderboard_enabled: Option, + pub editor_state_json: Option, + pub created_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct BarkBattleDraftConfigUpsertInput { + pub draft_id: String, + pub owner_user_id: String, + pub work_id: String, + pub config_version: u64, + pub ruleset_version: String, + pub difficulty_preset: String, + pub leaderboard_enabled: bool, + pub config_json: String, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct BarkBattleWorkPublishInput { + pub draft_id: String, + pub owner_user_id: String, + pub work_id: String, + pub published_snapshot_json: Option, + pub published_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct BarkBattleRuntimeConfigGetInput { + pub work_id: String, + pub owner_user_id: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct BarkBattleRunStartInput { + pub run_id: String, + pub run_token: String, + pub owner_user_id: String, + pub work_id: String, + pub config_version: u64, + pub ruleset_version: String, + pub difficulty_preset: String, + pub client_started_at_micros: i64, + pub server_started_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct BarkBattleRunFinishInput { + pub run_id: String, + pub run_token: String, + pub owner_user_id: String, + pub work_id: String, + pub config_version: u64, + pub ruleset_version: String, + pub difficulty_preset: String, + pub client_finished_at_micros: i64, + pub server_finished_at_micros: i64, + pub duration_ms: u64, + pub trigger_count: u64, + pub max_volume_millis: u32, + pub average_volume_millis: u32, + pub final_energy_millis: u32, + pub opponent_final_energy_millis: u32, + pub max_combo: u32, + pub metrics_json: String, + pub derived_metrics_json: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct BarkBattleRunGetInput { + pub run_id: String, + pub owner_user_id: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct BarkBattleProcedureResult { + pub ok: bool, + pub row_json: Option, + pub error_message: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattleEditorConfigSnapshot { + pub title: String, + pub description: String, + pub theme_preset: String, + pub player_dog_skin_preset: String, + pub opponent_dog_skin_preset: String, + pub difficulty_preset: String, + pub leaderboard_enabled: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattleDraftConfigSnapshot { + pub draft_id: String, + pub owner_user_id: String, + pub work_id: String, + pub config_version: u64, + pub ruleset_version: String, + pub difficulty_preset: String, + pub leaderboard_enabled: bool, + pub config_json: String, + pub editor_state_json: String, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattleRuntimeConfigSnapshot { + pub work_id: String, + pub owner_user_id: String, + pub source_draft_id: Option, + pub config_version: u64, + pub ruleset_version: String, + pub difficulty_preset: String, + pub leaderboard_enabled: bool, + pub config_json: String, + pub published_snapshot_json: String, + pub published_at_micros: i64, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BarkBattleRunSnapshot { + pub run_id: String, + pub owner_user_id: String, + pub work_id: String, + pub config_version: u64, + pub ruleset_version: String, + pub difficulty_preset: String, + pub leaderboard_enabled: bool, + pub status: String, + pub client_started_at_micros: i64, + pub server_started_at_micros: i64, + pub client_finished_at_micros: Option, + pub server_finished_at_micros: Option, + pub metrics_json: String, + pub server_result: Option, + pub validation_status: String, + pub anti_cheat_flags_json: String, + pub leaderboard_score: Option, + pub score_id: Option, +} diff --git a/server-rs/crates/spacetime-module/src/lib.rs b/server-rs/crates/spacetime-module/src/lib.rs index 2f5c64ca..0d207981 100644 --- a/server-rs/crates/spacetime-module/src/lib.rs +++ b/server-rs/crates/spacetime-module/src/lib.rs @@ -23,6 +23,7 @@ pub use spacetimedb::{ mod ai; mod asset_metadata; mod auth; +mod bark_battle; mod big_fish; mod custom_world; mod domain_types; @@ -38,6 +39,7 @@ mod visual_novel; pub use ai::*; pub use asset_metadata::*; pub use auth::*; +pub use bark_battle::*; pub use big_fish::*; pub use custom_world::*; pub use domain_types::*; diff --git a/server-rs/crates/spacetime-module/src/migration.rs b/server-rs/crates/spacetime-module/src/migration.rs index 28b9df3f..3881a17c 100644 --- a/server-rs/crates/spacetime-module/src/migration.rs +++ b/server-rs/crates/spacetime-module/src/migration.rs @@ -6,6 +6,11 @@ use spacetimedb::sats::de::serde::DeserializeWrapper; use spacetimedb::sats::ser::serde::SerializeWrapper; use std::collections::HashSet; +use crate::bark_battle::tables::{ + bark_battle_draft_config, bark_battle_leaderboard_entry, bark_battle_personal_best_projection, + bark_battle_published_config, bark_battle_runtime_run, bark_battle_score_record, + bark_battle_work_stats_projection, +}; use crate::big_fish::big_fish_runtime_run; use crate::match3d::tables::{ match3d_agent_message, match3d_agent_session, match3d_runtime_run, match3d_work_profile, @@ -216,6 +221,13 @@ macro_rules! migration_tables { puzzle_event, puzzle_runtime_run, puzzle_leaderboard_entry, + bark_battle_draft_config, + bark_battle_published_config, + bark_battle_runtime_run, + bark_battle_score_record, + bark_battle_leaderboard_entry, + bark_battle_work_stats_projection, + bark_battle_personal_best_projection, match3d_agent_session, match3d_agent_message, match3d_work_profile, diff --git a/server-rs/crates/spacetime-module/src/runtime/mod.rs b/server-rs/crates/spacetime-module/src/runtime/mod.rs index 52386257..730b89e2 100644 --- a/server-rs/crates/spacetime-module/src/runtime/mod.rs +++ b/server-rs/crates/spacetime-module/src/runtime/mod.rs @@ -1,13 +1,13 @@ pub mod analytics_date_dimension; -pub mod creation_entry_config; mod browse_history; +pub mod creation_entry_config; mod profile; mod settings; mod snapshots; pub use analytics_date_dimension::*; -pub use creation_entry_config::*; pub use browse_history::*; +pub use creation_entry_config::*; pub use profile::*; pub use settings::*; pub use snapshots::*; diff --git a/src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx b/src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx new file mode 100644 index 00000000..65e18de5 --- /dev/null +++ b/src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx @@ -0,0 +1,50 @@ +/* @vitest-environment jsdom */ + +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, expect, it, vi } from 'vitest'; + +import { BarkBattleConfigEditor } from './BarkBattleConfigEditor'; + +describe('BarkBattleConfigEditor', () => { + it('allows creators to edit lightweight config and publish a Bark Battle work', async () => { + const onPublish = vi.fn(); + render(); + + expect(screen.getByRole('heading', { name: '汪汪声浪大作战' })).toBeTruthy(); + expect(screen.getByText('轻配置作品')).toBeTruthy(); + expect((screen.getByLabelText('作品标题') as HTMLInputElement).value).toBe('我的声浪竞技场'); + expect((screen.getByLabelText('难度预设') as HTMLSelectElement).value).toBe('normal'); + expect((screen.getByLabelText('开启排行榜') as HTMLInputElement).checked).toBe(true); + + await userEvent.clear(screen.getByLabelText('作品标题')); + await userEvent.type(screen.getByLabelText('作品标题'), '周末狗狗杯'); + await userEvent.selectOptions(screen.getByLabelText('主题背景'), 'neon-park'); + await userEvent.selectOptions(screen.getByLabelText('玩家狗狗'), 'shiba'); + await userEvent.selectOptions(screen.getByLabelText('对手狗狗'), 'husky'); + await userEvent.selectOptions(screen.getByLabelText('难度预设'), 'hard'); + await userEvent.click(screen.getByLabelText('开启排行榜')); + await userEvent.click(screen.getByRole('button', { name: '发布并试玩' })); + + expect(onPublish).toHaveBeenCalledWith({ + title: '周末狗狗杯', + description: '', + themePreset: 'neon-park', + playerDogSkinPreset: 'shiba', + opponentDogSkinPreset: 'husky', + difficultyPreset: 'hard', + leaderboardEnabled: false, + }); + }); + + it('requires a non-empty title before publishing', async () => { + const onPublish = vi.fn(); + render(); + + await userEvent.clear(screen.getByLabelText('作品标题')); + await userEvent.click(screen.getByRole('button', { name: '发布并试玩' })); + + expect(onPublish).not.toHaveBeenCalled(); + expect(screen.getByText('请先填写作品标题')).toBeTruthy(); + }); +}); diff --git a/src/components/bark-battle-creation/BarkBattleConfigEditor.tsx b/src/components/bark-battle-creation/BarkBattleConfigEditor.tsx new file mode 100644 index 00000000..0b5b4bf5 --- /dev/null +++ b/src/components/bark-battle-creation/BarkBattleConfigEditor.tsx @@ -0,0 +1,161 @@ +import { useMemo, useState } from 'react'; + +import type { BarkBattleConfigEditorPayload } from '../../../packages/shared/src/contracts/barkBattle'; +import type { BarkBattleDifficultyPreset } from '../../../packages/shared/src/contracts/barkBattle'; +import { BarkBattlePreviewCard } from './BarkBattlePreviewCard'; + +export type BarkBattleConfigEditorProps = { + isBusy?: boolean; + onPublish: (payload: BarkBattleConfigEditorPayload) => void | Promise; + onBack?: () => void; +}; + +const THEME_OPTIONS = [ + { value: 'sunny-yard', label: '阳光院子' }, + { value: 'neon-park', label: '霓虹公园' }, + { value: 'moonlight-rooftop', label: '月光天台' }, +]; + +const DOG_SKIN_OPTIONS = [ + { value: 'corgi', label: '柯基' }, + { value: 'shiba', label: '柴犬' }, + { value: 'husky', label: '哈士奇' }, +]; + +const DIFFICULTY_OPTIONS: Array<{ value: BarkBattleDifficultyPreset; label: string }> = [ + { value: 'easy', label: '轻松' }, + { value: 'normal', label: '标准' }, + { value: 'hard', label: '硬核' }, +]; + +export function BarkBattleConfigEditor({ + isBusy = false, + onPublish, + onBack, +}: BarkBattleConfigEditorProps) { + const [title, setTitle] = useState('我的声浪竞技场'); + const [description, setDescription] = useState(''); + const [themePreset, setThemePreset] = useState('sunny-yard'); + const [playerDogSkinPreset, setPlayerDogSkinPreset] = useState('corgi'); + const [opponentDogSkinPreset, setOpponentDogSkinPreset] = useState('husky'); + const [difficultyPreset, setDifficultyPreset] = useState('normal'); + const [leaderboardEnabled, setLeaderboardEnabled] = useState(true); + const [error, setError] = useState(null); + + const payload = useMemo( + () => ({ + title: title.trim(), + description: description.trim(), + themePreset, + playerDogSkinPreset, + opponentDogSkinPreset, + difficultyPreset, + leaderboardEnabled, + }), + [ + title, + description, + themePreset, + playerDogSkinPreset, + opponentDogSkinPreset, + difficultyPreset, + leaderboardEnabled, + ], + ); + + const handlePublish = () => { + if (!payload.title) { + setError('请先填写作品标题'); + return; + } + setError(null); + void onPublish(payload); + }; + + return ( +
+
+
+
+
+

轻配置作品

+

汪汪声浪大作战

+

配置展示、皮肤、难度和排行榜;公平性规则由后端固定裁决。

+
+ {onBack ? ( + + ) : null} +
+ +
+ + +