diff --git a/.codegraph/.gitignore b/.codegraph/.gitignore new file mode 100644 index 00000000..9de0f169 --- /dev/null +++ b/.codegraph/.gitignore @@ -0,0 +1,16 @@ +# CodeGraph data files +# These are local to each machine and should not be committed + +# Database +*.db +*.db-wal +*.db-shm + +# Cache +cache/ + +# Logs +*.log + +# Hook markers +.dirty diff --git a/.codegraph/config.json b/.codegraph/config.json new file mode 100644 index 00000000..7af60ad8 --- /dev/null +++ b/.codegraph/config.json @@ -0,0 +1,143 @@ +{ + "version": 1, + "include": [ + "**/*.ts", + "**/*.tsx", + "**/*.js", + "**/*.jsx", + "**/*.py", + "**/*.go", + "**/*.rs", + "**/*.java", + "**/*.c", + "**/*.h", + "**/*.cpp", + "**/*.hpp", + "**/*.cc", + "**/*.cxx", + "**/*.cs", + "**/*.php", + "**/*.rb", + "**/*.swift", + "**/*.kt", + "**/*.kts", + "**/*.dart", + "**/*.svelte", + "**/*.vue", + "**/*.liquid", + "**/*.pas", + "**/*.dpr", + "**/*.dpk", + "**/*.lpr", + "**/*.dfm", + "**/*.fmx", + "**/*.scala", + "**/*.sc" + ], + "exclude": [ + "**/.git/**", + "**/node_modules/**", + "**/vendor/**", + "**/Pods/**", + "**/dist/**", + "**/build/**", + "**/out/**", + "**/bin/**", + "**/obj/**", + "**/target/**", + "**/*.min.js", + "**/*.bundle.js", + "**/.next/**", + "**/.nuxt/**", + "**/.svelte-kit/**", + "**/.output/**", + "**/.turbo/**", + "**/.cache/**", + "**/.parcel-cache/**", + "**/.vite/**", + "**/.astro/**", + "**/.docusaurus/**", + "**/.gatsby/**", + "**/.webpack/**", + "**/.nx/**", + "**/.yarn/cache/**", + "**/.pnpm-store/**", + "**/storybook-static/**", + "**/.expo/**", + "**/web-build/**", + "**/ios/Pods/**", + "**/ios/build/**", + "**/android/build/**", + "**/android/.gradle/**", + "**/__pycache__/**", + "**/.venv/**", + "**/venv/**", + "**/site-packages/**", + "**/dist-packages/**", + "**/.pytest_cache/**", + "**/.mypy_cache/**", + "**/.ruff_cache/**", + "**/.tox/**", + "**/.nox/**", + "**/*.egg-info/**", + "**/.eggs/**", + "**/go/pkg/mod/**", + "**/target/debug/**", + "**/target/release/**", + "**/.gradle/**", + "**/.m2/**", + "**/generated-sources/**", + "**/.kotlin/**", + "**/.dart_tool/**", + "**/.vs/**", + "**/.nuget/**", + "**/artifacts/**", + "**/publish/**", + "**/cmake-build-*/**", + "**/CMakeFiles/**", + "**/bazel-*/**", + "**/vcpkg_installed/**", + "**/.conan/**", + "**/Debug/**", + "**/Release/**", + "**/x64/**", + "**/.pio/**", + "**/release/**", + "**/*.app/**", + "**/*.asar", + "**/DerivedData/**", + "**/.build/**", + "**/.swiftpm/**", + "**/xcuserdata/**", + "**/Carthage/Build/**", + "**/SourcePackages/**", + "**/__history/**", + "**/__recovery/**", + "**/*.dcu", + "**/.composer/**", + "**/storage/framework/**", + "**/bootstrap/cache/**", + "**/.bundle/**", + "**/tmp/cache/**", + "**/public/assets/**", + "**/public/packs/**", + "**/.yardoc/**", + "**/coverage/**", + "**/htmlcov/**", + "**/.nyc_output/**", + "**/test-results/**", + "**/.coverage/**", + "**/.idea/**", + "**/logs/**", + "**/tmp/**", + "**/temp/**", + "**/_build/**", + "**/docs/_build/**", + "**/site/**" + ], + "languages": [], + "frameworks": [], + "maxFileSize": 1048576, + "extractDocstrings": true, + "trackCallSites": true +} \ No newline at end of file diff --git a/.codex-home-desktop-wait.png b/.codex-home-desktop-wait.png deleted file mode 100644 index 1cd523f7..00000000 Binary files a/.codex-home-desktop-wait.png and /dev/null differ diff --git a/.codex-home-desktop.png b/.codex-home-desktop.png deleted file mode 100644 index 68698ffa..00000000 Binary files a/.codex-home-desktop.png and /dev/null differ diff --git a/.codex-home-mobile-wait.png b/.codex-home-mobile-wait.png deleted file mode 100644 index d404bce4..00000000 Binary files a/.codex-home-mobile-wait.png and /dev/null differ diff --git a/.codex-home-mobile.png b/.codex-home-mobile.png deleted file mode 100644 index 1a06f613..00000000 Binary files a/.codex-home-mobile.png and /dev/null differ diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 00000000..7cef809b --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1,23 @@ +# Genarrative 项目级 Codex 配置。 +# 这里仅保存可进入仓库的 hook 配置与脚本;个人 token、MCP server、模型路由仍放在个人 ~/.codex/config.toml。 + +[features] +hooks = true + +# Codex 准备执行 git commit 前检查 TypeScript / admin-web / api-server 编译错误。 +# 脚本也可手动运行: +# node .codex/hooks/pre-submit-compile-check.mjs +[[hooks.PreToolUse]] +matcher = "Bash|shell_command|functions.shell_command" +command = "node .codex/hooks/pre-submit-compile-check.mjs" +timeout = 180 +statusMessage = "提交前检查编译错误" + +# Codex 每次工具修改文件后执行:同步 CodeGraph 索引。 +# 脚本也可手动运行: +# node .codex/hooks/post-edit-codegraph-sync.mjs +[[hooks.PostToolUse]] +matcher = "Bash|Edit|MultiEdit|Write|apply_patch|shell_command|functions.shell_command|functions.apply_patch" +command = "node .codex/hooks/post-edit-codegraph-sync.mjs" +timeout = 60 +statusMessage = "更新 CodeGraph 索引" diff --git a/.codex/hooks/post-edit-codegraph-sync.mjs b/.codex/hooks/post-edit-codegraph-sync.mjs new file mode 100644 index 00000000..864eb041 --- /dev/null +++ b/.codex/hooks/post-edit-codegraph-sync.mjs @@ -0,0 +1,51 @@ +#!/usr/bin/env node +import { spawnSync } from 'node:child_process'; +import { existsSync, mkdirSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(scriptDir, '..', '..'); +const logDir = resolve(repoRoot, '.codex', 'logs'); +const hasCodegraphConfig = existsSync(resolve(repoRoot, '.codegraph', 'config.json')); +const npmCommand = process.platform === 'win32' ? 'cmd' : 'npm'; + +if (!hasCodegraphConfig) { + console.log('[codex-hook] 未发现 .codegraph/config.json,跳过 CodeGraph 同步。'); + process.exit(0); +} + +const result = spawnSync(npmCommand, process.platform === 'win32' ? ['/d', '/s', '/c', 'npm run codegraph:sync'] : ['run', 'codegraph:sync'], { + cwd: repoRoot, + shell: false, + encoding: 'utf8', + env: { + ...process.env, + NO_COLOR: process.env.NO_COLOR ?? '1', + }, +}); + +mkdirSync(logDir, { recursive: true }); +if (result.stdout) { + process.stdout.write(result.stdout); +} +if (result.stderr) { + process.stderr.write(result.stderr); +} + +if (result.error) { + console.error(`[codex-hook] CodeGraph 同步启动失败:${result.error.message}`); + process.exit(1); +} + +if (result.signal) { + console.error(`[codex-hook] CodeGraph 同步被信号终止:${result.signal}`); + process.exit(1); +} + +if ((result.status ?? 0) !== 0) { + console.error('[codex-hook] CodeGraph 同步失败,请手动运行 npm run codegraph:sync 查看详情。'); + process.exit(result.status ?? 1); +} + +console.log('[codex-hook] CodeGraph 已同步。'); diff --git a/.codex/hooks/pre-submit-compile-check.mjs b/.codex/hooks/pre-submit-compile-check.mjs new file mode 100644 index 00000000..97a5b305 --- /dev/null +++ b/.codex/hooks/pre-submit-compile-check.mjs @@ -0,0 +1,122 @@ +#!/usr/bin/env node +import { spawnSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(scriptDir, '..', '..'); +const npmCommand = process.platform === 'win32' ? 'cmd' : 'npm'; +const hookInput = readHookInput(); + +if (hookInput && !isGitCommitCommand(extractShellCommand(hookInput))) { + process.exit(0); +} + +const validationSteps = [ + { + label: 'TypeScript typecheck', + command: npmCommand, + args: process.platform === 'win32' ? ['/d', '/s', '/c', 'npm run typecheck'] : ['run', 'typecheck'], + }, + { + label: 'Admin web typecheck', + command: npmCommand, + args: process.platform === 'win32' ? ['/d', '/s', '/c', 'npm run admin-web:typecheck'] : ['run', 'admin-web:typecheck'], + }, + { + label: 'Rust api-server compile check', + command: 'cargo', + args: ['check', '-p', 'api-server', '--manifest-path', 'server-rs/Cargo.toml'], + }, +]; + +for (const step of validationSteps) { + const result = runStep(step); + if (!result.ok) { + const reason = `[codex-hook] 提交前编译检查失败:${step.label}。请修复编译错误后再提交。`; + console.error(reason); + if (hookInput) { + console.log(JSON.stringify({ decision: 'block', reason })); + process.exit(0); + } + process.exit(result.status ?? 1); + } +} + +console.error('[codex-hook] 提交前编译检查通过。'); + +function runStep(step) { + console.error(`[codex-hook] ${step.label}`); + const result = spawnSync(step.command, step.args, { + cwd: repoRoot, + shell: false, + encoding: 'utf8', + env: { + ...process.env, + NO_COLOR: process.env.NO_COLOR ?? '1', + }, + }); + + if (result.stdout) { + process.stderr.write(result.stdout); + } + if (result.stderr) { + process.stderr.write(result.stderr); + } + + if (result.error) { + console.error(`[codex-hook] ${step.label} 启动失败:${result.error.message}`); + return { ok: false, status: 1 }; + } + + if (result.signal) { + console.error(`[codex-hook] ${step.label} 被信号终止:${result.signal}`); + return { ok: false, status: 1 }; + } + + return { + ok: (result.status ?? 0) === 0, + status: result.status ?? 1, + }; +} + +function readHookInput() { + try { + const rawInput = readFileSync(0, 'utf8').trim(); + if (!rawInput) { + return null; + } + return JSON.parse(rawInput); + } catch { + return null; + } +} + +function extractShellCommand(input) { + const candidates = [ + input?.tool_input?.command, + input?.toolInput?.command, + input?.tool_args?.command, + input?.toolArgs?.command, + input?.arguments?.command, + input?.params?.command, + input?.command, + ]; + + const command = candidates.find(value => typeof value === 'string' && value.trim().length > 0); + if (command) { + return command; + } + + const shellCommand = input?.tool_input?.cmd ?? input?.toolInput?.cmd ?? input?.arguments?.cmd; + if (Array.isArray(shellCommand)) { + return shellCommand.join(' '); + } + + return ''; +} + +function isGitCommitCommand(command) { + return /(^|[;&|]\s*)git(?:\.exe)?\b[\s\S]{0,200}\bcommit\b/iu.test(command); +} diff --git a/.codex/logs/run-dev-web-final.ps1 b/.codex/logs/run-dev-web-final.ps1 deleted file mode 100644 index 196bc9f0..00000000 --- a/.codex/logs/run-dev-web-final.ps1 +++ /dev/null @@ -1,4 +0,0 @@ -Set-Location 'C:\Genarrative' -$env:RUST_SERVER_TARGET = 'http://127.0.0.1:8082' -$env:GENARRATIVE_RUNTIME_SERVER_TARGET = 'http://127.0.0.1:8082' -npm.cmd run dev:web *> 'C:\Genarrative\.codex\logs\dev-web-final.out.log' diff --git a/.codex/skills/behavior-driven-development b/.codex/skills/behavior-driven-development deleted file mode 120000 index 1db643f5..00000000 --- a/.codex/skills/behavior-driven-development +++ /dev/null @@ -1 +0,0 @@ -../../.hermes/skills/behavior-driven-development/ \ No newline at end of file diff --git a/.codex/skills/genarrative-play-type-integration/SKILL.md b/.codex/skills/genarrative-play-type-integration/SKILL.md index 1e3ab0c6..63d14982 100644 --- a/.codex/skills/genarrative-play-type-integration/SKILL.md +++ b/.codex/skills/genarrative-play-type-integration/SKILL.md @@ -7,35 +7,46 @@ metadata: version: "1.0" --- -# Genarrative 新增玩法类型接入流程 +# Genarrative 新增玩法创作工具平台 SOP -用于在 Genarrative 中新增一个创作入口/玩法类型,而不是单纯说明用户如何从入口创建作品。 +把新增玩法当成平台能力接入,不把任何既有玩法当作默认模板。先确定通用模式和契约,再写具体玩法代码。 -## 适用场景 +## 硬性禁区 -- 新增一个游戏玩法入口 -- 让某个玩法从“敬请期待”变为可创建 -- 为新玩法补齐创作工作台、结果页、发布与试玩链路 -- 将新玩法接入创作中心作品架与广场 +- 不恢复前端硬编码入口配置;创作入口事实源必须来自 SpacetimeDB 和 `/api/creation-entry/config`。 +- 不把聊天输入区、流式消息或轻输入 Agent 作为新增玩法默认工作台。 +- 不在新页面内手写图片上传、参考图、AI 重绘、历史图选择、预览或删除确认逻辑。 +- 不把通用系列素材建模成任一玩法专属 DTO;玩法只能追加自己的运行态字段。 +- 不让前端承接正式业务真相;发布、试玩、通关、失败、计分、资产持久化和作品状态以后端投影为准。 +- 不新建平行入口系统、平行作品架或平行公开列表;优先扩展现有平台壳、现有阶段和现有聚合。 +- 不在 UI 面板内默认写功能说明、规则说明或开发解释文案。 -## 先判断接入级别 +## 接入前输入 -### 1. 只做入口占位 +开始编码前,PRD 或当前玩法文档必须已经明确: -只需要新增入口配置,不接 session/workspace/result/runtime。 +- `playId`、对外名称、工程域名、入口 `visible/open` 状态。 +- 创作链路:入口 -> 工作台 -> 生成页 -> 结果页 -> 试玩 -> 发布 -> 运行态。 +- 表单字段:字段名、默认值、校验、后端落库位置、生成提示词来源。 +- 单图资产槽位:`slotId`、`slotType`、`slotName`、提示词来源、读取字段、写回字段、是否允许历史图和 AI 重绘。 +- 系列素材槽位:`batchId` 语义、`sheetSpec`、`slotSpecs`、切图规则、透明化规则、失败回写、局部重生成策略。 +- API 命名空间:`/api/creation//sessions`、`actions`、`works`、`runtime`。 +- 草稿恢复、生成中恢复、失败重试、登录切换、发布后回读和移动端行为。 +- 验证命令和例外声明;没有例外时写明“无创作工具模式例外”。 -适合: +## 默认模式 -- 敬请期待 -- 灰度占位 +新增玩法默认采用表单/图片输入创作工作台: -### 2. 可进入创作工作台 +```text +创作入口 -> 表单/图片输入工作台 -> 生成页 -> 结果页 -> 试玩 -> 发布 -> 运行态 +``` -需要补齐前端分流、session、工作台、结果页,至少能生成草稿。 +工作台只提交结构化表单、图片槽位和配置 payload。确需自然语言对话时,先走“例外流程”,不能把聊天区直接加进默认工作台。 -### 3. 完整玩法闭环 +## SOP -需要补齐: +### 1. 文档和领域词先行 - 创作入口 - 工作台 @@ -78,345 +89,208 @@ metadata: 11. **规则参数归属**:哪些配置是创作者可编辑;哪些阈值、时长、冷却、计分、反作弊、裁决规则必须留在后端规则集。 12. **旧数据策略**:旧草稿、旧发布配置、旧分享码是迁移、降级展示、重新生成,还是明确不兼容。 -## 推荐接入顺序 +- `AGENTS.md` +- `.hermes/shared-memory/` +- `CONTEXT.md` +- `docs/README.md` +- `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md` +- 相关玩法 PRD 或设计文档 -### Step 1: 先定玩法 ID 和能力边界 +如果文档不能精确指导字段、契约、资产槽位、生成流程和恢复语义,先补文档再编码。新增长期约定时同步 `.hermes/shared-memory/`。 -先明确: +### 2. 定玩法边界 -- `id` 是什么 -- 入口是否可见 -- 是否可点击创建 -- 是否需要对话式创作 -- 是否需要生成中页面 -- 是否需要 result/runtime/gallery/share +固定 `playId`、对外名称、工程域、入口状态、是否支持结果页、试玩、发布、作品架、广场、分享和 runtime。不要先用临时 ID 接线后再批量改名。 -不要先随便起临时 ID 再改名。 +### 3. 接入口配置 -### Step 2: 新增入口配置 +入口配置事实源是 SpacetimeDB `creation_entry_type_config`。后台通过 `/admin/api/creation-entry/config` 管理,前台通过 `/api/creation-entry/config` 读取。 -文件: +前端只允许在展示层派生: -- `src/config/newWorkEntryConfig.ts` +- 可见入口卡片。 +- 锁定或开放状态。 +- 排序、图标、短标题等展示信息。 -在 `creationTypes` 中新增: +`api-server` 路由熔断必须使用同一份入口配置。禁止新增或恢复前端本地默认入口配置作为事实源。 -- `id` -- `title` -- `subtitle` -- `badge` -- `visible` -- `open` +### 4. 前端阶段 -如果只是占位: +按需要扩展 `SelectionStage`: -- `visible: true` -- `open: false` +- `-workspace` +- `-generating` +- `-result` +- `-runtime` +- `-gallery-detail` -### Step 3: 确认类型过滤逻辑 +阶段名可以按玩法命名,UI 形态必须仍是表单/图片创作工作台。进入工作台时只初始化结构化草稿状态,不启动默认聊天会话。 -文件: +### 5. 工作台实现 -- `src/components/platform-entry/platformEntryCreationTypes.ts` +工作台必须满足: -检查: +- 使用表单控件、图片槽位、风格选项、难度选项、开关和提交按钮组织输入。 +- 单图槽位统一使用 `CreativeImageInputPanel`。 +- 组件缺少能力时先扩展 `CreativeImageInputPanel` 的受控 props,不在玩法页面复制上传、参考图、AI 重绘、历史图、预览或删除确认。 +- 主图读取、裁剪、历史素材弹层、计费确认、自动保存和后端请求由外层页面持有;通用面板只表达输入 UI 和短生命周期 UI 状态。 +- 提交 payload 必须是表单字段与图片槽位结构,不是用户消息文本。 -- `getVisiblePlatformCreationTypes()` 是否能展示新类型 -- `isPlatformCreationTypeVisible()` 是否能识别新类型 -- `locked` / `hidden` 是否正确映射 +### 6. 单图资产槽位 -### Step 4: 扩展页面阶段 +角色形象、UI 背景、容器、封面、分享图、图标等单张图都按单图资产槽位处理。 -文件: +统一约定: -- `src/components/platform-entry/platformEntryTypes.ts` +- 槽位用 `slotId` 稳定标识,`slotType` 表达用途,`slotName` 用于 UI 标签。 +- 上传图、参考图、AI 重绘、历史图选择和删除确认都通过 `CreativeImageInputPanel` 入口表达。 +- 后端写回 `imageSrc`、`imageObjectKey`、`assetObjectId` 中可用字段;前端展示前通过平台资产读取能力换签。 +- 单个槽位重生成只禁用该槽位动作,不阻塞结果页其它槽位、系列素材槽位或导航。 -为新玩法补充 `SelectionStage`: +### 7. 系列素材图集生成 -- `*-agent-workspace` -- `*-generating`(可选) -- `*-result` -- `*-runtime`(可选) -- `*-gallery-detail`(可选) +地块、物品、障碍、装饰、UI 部件等一组同类素材都走通用系列素材图集生成流程: -### Step 5: 在总流程中加类型分流 +```text +批量规划 -> sheet 生图 -> 后端切图 -> 去背景/透明化 -> PNG 输出 -> OSS 持久化 -> 状态回写 -> 局部重生成 +``` -文件: +玩法只提供: -- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx` +- `sheetSpec`:画布比例、行列、单格尺寸、输出格式、背景处理策略。 +- `slotSpecs`:每个素材槽位的 `slotId`、`slotType`、`slotName`、提示词、sheet 单元格映射。 +- 玩法字段映射:把通用素材结果映射回玩法自己的 draft/profile/runtime 字段。 -在 `handleCreationHubCreateType(type)` 中新增分支,确保: +通用系列素材结果建议字段: -- 能进入对应工作台 -- 能设置对应 `selectionStage` -- 能关闭类型弹层 - -同时按玩法补齐: - -- `openAgentWorkspace()` -- `leaveFlow()` -- `submitMessage()`(对话式玩法) -- `executeAction()` - -### Step 6: 接入通用 Agent flow controller - -文件: - -- `src/components/platform-entry/usePlatformCreationAgentFlowController.ts` - -如果是 Agent 型玩法,复用通用控制器: - -- `createSession` -- `getSession` -- `streamMessage` -- `executeAction` -- `isBusy` +- `batchId` +- `slotId` +- `slotType` +- `slotName` +- `prompt` +- `imageSrc` +- `imageObjectKey` +- `assetObjectId` +- `sourceSheetCell` +- `status` - `error` -- `streamingReplyText` -- `selectionStage` 切换 -### Step 7: 定义 shared contracts +玩法可追加运行态字段,例如半径、宽度、视图索引或碰撞参数,但不能依赖任何玩法专属字段作为平台通用模型。新增玩法 compile action 内部调用通用系列素材服务;如果通用服务还缺能力,先补通用服务再接玩法。 -前端: +### 8. 契约与 API + +前后端必须同步补契约: - `packages/shared/src/contracts/` - -后端: - - `server-rs/crates/shared-contracts/src/` -至少补齐: +玩法 API 保留独立命名空间: -- session snapshot -- create session request/response -- message request/response -- action request/response -- draft/result 结构 -- work summary / gallery 结构(如果需要) -- runtime 结构(如果需要) +- `POST /api/creation//sessions` +- `GET /api/creation//sessions/{sessionId}` +- `POST /api/creation//sessions/{sessionId}/actions` +- `/api/creation//works` +- `/api/creation//runtime` -### Step 8: 实现前端 service client +契约需要区分: -目录参考: +- 工作台输入。 +- 草稿 snapshot。 +- 单图资产槽位。 +- 系列素材批次与槽位。 +- 结果页操作。 +- 发布作品摘要。 +- runtime snapshot。 -- `src/services/` +### 9. 后端分层 -按玩法补: +按 DDD 边界落地: -- creation client -- runtime client(可选) -- works client(可选) -- gallery client(可选) +- `module-`:纯领域规则、状态机、draft/runtime 校验。 +- `shared-contracts`:前后端 DTO。 +- `spacetime-module`:表、reducer、procedure、事务编排、migration。 +- `spacetime-client`:typed facade 和 row mapper。 +- `api-server`:Axum 路由、鉴权、BFF、SSE、生成编排。 +- `platform-*`:LLM、图片生成、OSS、认证等外部副作用。 -建议保持和现有玩法一致的 API base 与命名风格。 +涉及 SpacetimeDB schema 时同步 `migration.rs`、表目录和绑定,并运行 `npm run check:spacetime-schema`。 -### Step 9: 接后端 API - -文件参考: - -- `server-rs/crates/api-server/src/puzzle.rs` -- `server-rs/crates/api-server/src/puzzle_agent_turn.rs` -- `server-rs/crates/api-server/src/match3d.rs` - -通常需要: - -- create session -- get session -- send message -- stream message -- execute action -- publish / save / delete -- runtime start / action(可选) -- gallery / detail(可选) - -后端设计优先按 Genarrative 的 DDD 分层拆开,不要把玩法规则、数据库事务、LLM 调用和 HTTP handler 混在一个文件里: - -- `module-`:纯领域规则、状态机、draft/runtime 校验,不依赖 Axum、SpacetimeDB 或外部平台。 -- `shared-contracts`:前后端 DTO、请求/响应、session snapshot、draft/result/runtime 结构。 -- `spacetime-module`:表定义、reducer/procedure、事务编排、migration;表结构变化要同步生成绑定。 -- `spacetime-client`:api-server 到 SpacetimeDB 的 facade,隐藏 reducer 调用细节。 -- `api-server`:Axum 路由、鉴权、SSE/stream、应用层编排。 -- `platform-*`:LLM、资产上传、鉴权、第三方服务等副作用。 - -建议按四条线设计后端能力: - -- Agent 创作线:session、turn、stream、compile action。 -- Works 作品线:保存、发布、删除、草稿恢复。 -- Gallery 广场线:公开列表、详情、like/remix/share。 -- Runtime 运行态线:开始试玩、提交动作、读取状态。 - -### Step 10: 新增工作台组件 - -目录建议: - -- `src/components/-creation/AgentWorkspace.tsx` - -两种形态: - -#### 对话式 - -适合设定逐轮补齐。 - -参考: - -- `BigFishAgentWorkspace.tsx` -- `Match3DAgentWorkspace.tsx` - -#### 表单式 - -适合输入结构明确的玩法。 - -参考: - -- `PuzzleAgentWorkspace.tsx` - -### Step 11: 在渲染树中挂载新页面 - -文件: - -- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx` - -补齐: - -- workspace 分支 -- generating 分支(如需要) -- result 分支 -- runtime 分支(如需要) - -### Step 12: 新增结果页 - -目录建议: - -- `src/components/-result/ResultView.tsx` +### 10. 结果页 结果页至少支持: -- 展示 draft -- 返回编辑 -- 发布 -- 试玩 -- 错误展示 +- 展示草稿和生成状态。 +- 返回工作台编辑。 +- 单图槽位重生成。 +- 系列素材追加、替换、局部重生成。 +- 发布。 +- 试玩。 +- 错误展示和失败重试。 -### Step 13: 需要试玩就补 runtime +单图槽位和系列素材槽位的生成状态互不阻塞。已有可查看结果时,局部重生成不能把作品架草稿重新变成不可打开的全局生成中。 -目录建议: +### 11. 运行态、作品架和广场 -- `src/components/-runtime/RuntimeShell.tsx` +需要试玩或发布时补齐: -如果玩法是游戏类,建议补完整 runtime 闭环。 +- runtime start/action/finish API。 +- 作品保存、发布、删除、回读。 +- 作品架摘要。 +- 公开列表、详情、分享码。 +- 公开列表优先消费后端投影或 BFF 缓存,不让前端直接拼源表事实。 -### Step 14: 接入作品架 / 广场 / 分享 +运行态可以做低延迟表现,但正式胜负、分数、奖励、排行榜和发布状态以后端裁决为准。 -需要改: +### 12. 恢复与登录态 -- `src/components/custom-world-home/creationWorkShelf.ts` -- `src/components/custom-world-home/CustomWorldCreationHub.tsx` -- `src/services/publicWorkCode.ts` +必须处理: -如果玩法支持发布,还要补: +- 刷新恢复生成中草稿。 +- 生成页计时从后端摘要时间恢复。 +- 失败后回读 session/work detail 再决定是否展示失败。 +- 退出登录清空私有玩法状态。 +- 私有生成图展示前换签。 +- result/runtime 缺必要 draft 时回到可恢复入口,不停在空白页。 -- public work code -- public detail -- publish share modal -- like/remix(可选) +### 13. 例外流程 -### Step 15: 处理登录态与草稿恢复 +任何非表单/图片工作台、对话式 Agent、独立创作系统或特殊资产模型都必须先更新 PRD 和平台文档。例外声明至少写清: -要考虑: +- 为什么默认表单/图片工作台不能满足。 +- 例外影响哪些输入、契约、后端流程和测试。 +- 如何保留单图资产槽位和系列素材槽位的通用能力。 +- 如何回退到平台默认链路。 -- 刷新恢复草稿 -- 退出登录清空私有状态 -- result/draft 缺失时回退 -- busy / generating / runtime 中断恢复 +没有文档例外,不进入编码。 -### Step 16: 补测试 +## PRD 检查块 -至少覆盖: +在新增玩法 PRD 中保留这一段: -- 入口展示 -- 类型分流 -- 工作台打开 -- session 创建 -- compile action -- result 页切换 -- 发布后刷新作品架 -- runtime 进入与退出 +```md +## 创作工具平台接入声明 -## 最小改动清单 - -### 只做占位 - -只改: - -- `src/config/newWorkEntryConfig.ts` - -### 做到可进入工作台 - -至少改: - -- `newWorkEntryConfig.ts` -- `platformEntryTypes.ts` -- `PlatformEntryFlowShellImpl.tsx` -- 新玩法 service client -- 新玩法工作台组件 -- shared contracts -- 后端 API - -### 做到完整闭环 - -还要补: - -- result 页 -- runtime -- works / gallery -- public code -- share -- 作品架聚合 -- 测试 - -## 常见坑 - -1. 只加入口配置不够,类型分流和页面阶段也要补。 -2. `SelectionStage` 不扩展,前端无法安全切页。 -3. 新玩法如果要出现在作品架,必须改聚合逻辑,不只是加入口。 -4. 发布后不刷新 works/gallery,用户会看不到新作品。 -5. 如果走 SpacetimeDB,表结构变化要同步 migration 和绑定;`spacetime-client/src/module_bindings/` 通常是生成物,不要为了修编译或格式化而手改,优先改 module 源 schema/reducer/procedure 后重新生成。 -6. 做 analytics/tracking 这类 runtime 能力时,不要只补 API DTO;先在 `module-runtime` 写纯函数测试(例如 day/week/month/quarter/year bucket 聚合、scope/event 过滤),RED 后再补领域类型与聚合函数。 -7. 时间粒度聚合建议复用已有 date dimension 逻辑,把 daily stat 映射到 day/week/month/quarter/year bucket;bucket 输出要有稳定排序,并显式携带 `bucketKey`、`bucketStartDateKey`、`bucketEndDateKey`、`value`。 -8. 后端 shared-contracts 与前端 `packages/shared/src/contracts/runtime.ts` 要同步补 request/response/type union;admin-web 若有独立 `api/adminApiTypes.ts`,也要同步,避免共享包已更新但管理端本地类型缺失。 -9. 退出登录时要清空新玩法私有状态,避免串用户。 -10. 移动端入口卡片增多后要检查布局和滚动体验。 - -## 验证标准 - -一个玩法算真正接入成功,至少要满足: - -- 入口能展示 -- 能进入对应工作台 -- 能创建 session -- 能生成草稿 -- 能进入结果页 -- 能返回编辑 -- 如果需要,可试玩 -- 如果需要,可发布 -- 发布后能回到作品架 / 广场 / 分享链路 - -## 建议验证命令 - -按改动范围选择: - -```bash -# 后端 contracts / module-runtime / api-server -cd server-rs -cargo test -p shared-contracts -cargo test -p module-runtime -cargo check -p api-server - -# SpacetimeDB schema/reducer/procedure 改动后,优先在有 CLI 的机器重新生成 bindings -npm run spacetime:generate -- --rust-only - -# 前端类型 -npm run admin-web:typecheck +- 工作台模式:表单/图片输入创作工作台 +- 创作链路:入口 -> 工作台 -> 生成页 -> 结果页 -> 试玩 -> 发布 -> 运行态 +- 单图资产槽位: + - slotId / slotType / slotName / 提示词来源 / 写回字段 / 是否允许历史图 / 是否允许 AI 重绘 +- 系列素材槽位: + - batchId / sheetSpec / slotSpecs / 切图规则 / 透明化规则 / 失败回写 / 局部重生成 +- API 命名空间:/api/creation//... +- 业务真相:后端裁决字段和前端表现字段边界 +- 创作工具模式例外:无;如有,先写明例外原因和回退方式 +- 验证命令: ``` -如果新增完整前端玩法闭环,还要按项目实际脚本补充 web typecheck、lint 或 Playwright/单元测试。 +## 验证门禁 + +按改动范围运行: + +- `npm run check:encoding` +- `npm run typecheck` +- 前端工作台测试:确认没有聊天式 Agent 输入,提交的是表单/图片 payload。 +- `CreativeImageInputPanel` 测试:覆盖多玩法标签、上传、AI 重绘、参考图上限、历史图入口和删除确认。 +- 系列素材测试:覆盖 sheet layout、切图、透明化、OSS 持久化、追加、替换、局部重生成和失败回写。 +- 结果页测试:覆盖单图槽位重生成和系列素材槽位重生成互不阻塞。 +- 后端定向测试:覆盖 compile action、资产持久化、失败回写、发布和 runtime start。 +- 涉及 SpacetimeDB schema 时运行 `npm run check:spacetime-schema`。 diff --git a/.codex/skills/gpt-image-2-apimart/SKILL.md b/.codex/skills/gpt-image-2-apimart/SKILL.md index 2cc40454..99caa76b 100644 --- a/.codex/skills/gpt-image-2-apimart/SKILL.md +++ b/.codex/skills/gpt-image-2-apimart/SKILL.md @@ -5,7 +5,7 @@ description: Generate or inspect project image assets through this repository's # gpt-image-2 VectorEngine -Use this skill for project-local image asset generation that must match the repository's `server-rs` VectorEngine `gpt-image-2-all` path. The folder still contains `apimart` in its name for compatibility with existing local plugin references. +Use this skill for project-local image asset generation that must match the repository's `server-rs` VectorEngine `gpt-image-2` path. The folder still contains `apimart` in its name for compatibility with existing local plugin references. ## Workflow @@ -40,22 +40,14 @@ Default body: ```json { - "model": "gpt-image-2-all", + "model": "gpt-image-2", "prompt": "", "n": 1, "size": "1024x1024" } ``` -For weak visual references in text-to-image generation, add: - -```json -{ - "image": ["data:image/png;base64,..."] -} -``` - -For image-to-image work that must follow a reference image closely, use the VectorEngine edits endpoint instead of the generations `image` array: +For visual references, use the edit endpoint instead of the create endpoint: ```text POST {VECTOR_ENGINE_BASE_URL}/v1/images/edits @@ -73,9 +65,9 @@ size=1024x1024 image=@reference.png ``` -Prefer edits for workflows where the reference image controls composition, pose, container shape, or layout. In this repository, Match3D container UI generation uses edits with `public/match3d-background-references/pot-fused-reference.png` as the `image` part. +In this repository, calls with no reference images use `POST /v1/images/generations`; calls with any reference image use `POST /v1/images/edits` and pass references as one or more `image` form parts. Match3D container UI generation embeds `public/match3d-background-references/pot-fused-reference.png` into the edit request as an `image` part. -Accept image output from `data[].url`, `data[].b64_json`, or direct nested `url` fields. VectorEngine GPT-image-2-all currently returns synchronously; do not poll APIMart task endpoints. +Accept image output from `data[].url`, `data[].b64_json`, or direct nested `url` fields. VectorEngine GPT-image-2 currently returns synchronously; do not poll APIMart task endpoints. ## Environment diff --git a/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs b/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs index 1ba9eb69..e3b1f99d 100644 --- a/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs +++ b/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs @@ -245,7 +245,7 @@ async function downloadUrl(url, timeoutMs) { async function generateOne(env, entry, outDir) { const requestBody = { - model: 'gpt-image-2-all', + model: 'gpt-image-2', prompt: buildPrompt(entry), n: 1, size: '1024x1024', @@ -305,7 +305,7 @@ if (dryRun) { id: entry.id, title: entry.title, body: { - model: 'gpt-image-2-all', + model: 'gpt-image-2', prompt: buildPrompt(entry), n: 1, size: '1024x1024', diff --git a/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs b/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs index 72e05646..165dac01 100644 --- a/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs +++ b/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs @@ -211,7 +211,7 @@ async function downloadUrl(url, timeoutMs) { async function generateOne(env, template, outDir) { const requestBody = { - model: 'gpt-image-2-all', + model: 'gpt-image-2', prompt: buildPrompt(template), n: 1, size: '1024x1024', @@ -275,7 +275,7 @@ if (dryRun) { id: template.id, title: template.title, body: { - model: 'gpt-image-2-all', + model: 'gpt-image-2', prompt: buildPrompt(template), n: 1, size: '1024x1024', diff --git a/.codex/tmp-schema.json b/.codex/tmp-schema.json deleted file mode 100644 index abadebbf..00000000 --- a/.codex/tmp-schema.json +++ /dev/null @@ -1,35281 +0,0 @@ -{ - "typespace": { - "types": [ - { - "Product": { - "elements": [ - { - "name": { - "some": "quest_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "story_session_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "actor_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "issuer_npc_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "issuer_npc_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "scene_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "chapter_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "act_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "thread_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "contract_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "title" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "description" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "summary" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "status" - }, - "algebraic_type": { - "Ref": 1 - } - }, - { - "name": { - "some": "completion_notified" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "reward" - }, - "algebraic_type": { - "Ref": 2 - } - }, - { - "name": { - "some": "reward_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "narrative_binding" - }, - "algebraic_type": { - "Ref": 7 - } - }, - { - "name": { - "some": "steps" - }, - "algebraic_type": { - "Array": { - "Ref": 10 - } - } - }, - { - "name": { - "some": "active_step_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "visible_stage" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "hidden_flags" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "discovered_fact_ids" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "related_carrier_ids" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "consequence_ids" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "active" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "readyToTurnIn" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "completed" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "turnedIn" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "failed" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "expired" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "affinity_bonus" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "currency" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "experience" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "items" - }, - "algebraic_type": { - "Array": { - "Ref": 3 - } - } - }, - { - "name": { - "some": "intel" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 6 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "story_hint" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "item_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "category" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "description" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "quantity" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "rarity" - }, - "algebraic_type": { - "Ref": 4 - } - }, - { - "name": { - "some": "tags" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "stackable" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "stack_key" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "equipment_slot_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 5 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "common" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "uncommon" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "rare" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "epic" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "legendary" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "weapon" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "armor" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "relic" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "rumor_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "unlocked_scene_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "origin" - }, - "algebraic_type": { - "Ref": 8 - } - }, - { - "name": { - "some": "narrative_type" - }, - "algebraic_type": { - "Ref": 9 - } - }, - { - "name": { - "some": "dramatic_need" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "issuer_goal" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "player_hook" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "world_reason" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "followup_hooks" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "aiCompiled" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "fallbackBuilder" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "bounty" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "escort" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "investigation" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "retrieval" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "relationship" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "trial" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "step_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "kind" - }, - "algebraic_type": { - "Ref": 11 - } - }, - { - "name": { - "some": "target_hostile_npc_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "target_npc_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "target_scene_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "target_item_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "required_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "progress" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "title" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "reveal_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "complete_text" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "defeatHostileNpc" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "inspectTreasure" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "sparWithNpc" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "talkToNpc" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "reachScene" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "deliverItem" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "quest_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "run_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "advanced_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "run_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "result_reference_row_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "result_ref_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "task_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "reference_kind" - }, - "algebraic_type": { - "Ref": 16 - } - }, - { - "name": { - "some": "reference_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "label" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "storySession" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "storyEvent" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "customWorldProfile" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "questRecord" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "runtimeItemRecord" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "assetObject" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "task_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "task_kind" - }, - "algebraic_type": { - "Ref": 18 - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "request_label" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "source_module" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "source_entity_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "request_payload_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "status" - }, - "algebraic_type": { - "Ref": 19 - } - }, - { - "name": { - "some": "failure_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "latest_text_output" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "latest_structured_payload_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "version" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "started_at" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "completed_at" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "storyGeneration" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "characterChat" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "npcChat" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "customWorldGeneration" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "questIntent" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "runtimeItemIntent" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "pending" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "running" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "completed" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "failed" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "cancelled" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "task_stage_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "task_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "stage_kind" - }, - "algebraic_type": { - "Ref": 21 - } - }, - { - "name": { - "some": "label" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "detail" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "stage_order" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "status" - }, - "algebraic_type": { - "Ref": 22 - } - }, - { - "name": { - "some": "text_output" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "structured_payload_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "warning_messages" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "started_at" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "completed_at" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "preparePrompt" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "requestModel" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "repairResponse" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "normalizeResult" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "persistResult" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "pending" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "running" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "completed" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "skipped" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "text_chunk_row_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "chunk_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "task_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "stage_kind" - }, - "algebraic_type": { - "Ref": 21 - } - }, - { - "name": { - "some": "sequence" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "delta_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "task_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "stage_kind" - }, - "algebraic_type": { - "Ref": 21 - } - }, - { - "name": { - "some": "sequence" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "delta_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "task" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 26 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "text_chunk" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 29 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "task_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "task_kind" - }, - "algebraic_type": { - "Ref": 18 - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "request_label" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "source_module" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "source_entity_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "request_payload_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "status" - }, - "algebraic_type": { - "Ref": 19 - } - }, - { - "name": { - "some": "failure_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "stages" - }, - "algebraic_type": { - "Array": { - "Ref": 27 - } - } - }, - { - "name": { - "some": "result_references" - }, - "algebraic_type": { - "Array": { - "Ref": 28 - } - } - }, - { - "name": { - "some": "latest_text_output" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "latest_structured_payload_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "version" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "started_at_micros" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "completed_at_micros" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "stage_kind" - }, - "algebraic_type": { - "Ref": 21 - } - }, - { - "name": { - "some": "label" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "detail" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "order" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "status" - }, - "algebraic_type": { - "Ref": 22 - } - }, - { - "name": { - "some": "text_output" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "structured_payload_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "warning_messages" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "started_at_micros" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "completed_at_micros" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "result_ref_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "task_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "reference_kind" - }, - "algebraic_type": { - "Ref": 16 - } - }, - { - "name": { - "some": "reference_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "label" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "chunk_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "task_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "stage_kind" - }, - "algebraic_type": { - "Ref": 21 - } - }, - { - "name": { - "some": "sequence" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "delta_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "chapter_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "granted_quest_xp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "granted_hostile_xp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "hostile_defeat_increment" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "level_at_exit" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "record" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 32 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "chapter_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "chapter_index" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "total_chapters" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "entry_pseudo_level_millis" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "exit_pseudo_level_millis" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "entry_level" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "exit_level" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "planned_total_xp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "planned_quest_xp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "planned_hostile_xp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "actual_quest_xp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "actual_hostile_xp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "expected_hostile_defeat_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "actual_hostile_defeat_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "level_at_entry" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "level_at_exit" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "pace_band" - }, - "algebraic_type": { - "Ref": 33 - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "openingFast" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "steady" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "pressure" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "finaleDense" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "mutation_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "story_session_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "actor_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "mutation" - }, - "algebraic_type": { - "Ref": 35 - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "grantItem" - }, - "algebraic_type": { - "Ref": 36 - } - }, - { - "name": { - "some": "consumeItem" - }, - "algebraic_type": { - "Ref": 41 - } - }, - { - "name": { - "some": "equipItem" - }, - "algebraic_type": { - "Ref": 42 - } - }, - { - "name": { - "some": "unequipItem" - }, - "algebraic_type": { - "Ref": 43 - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "slot_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "item" - }, - "algebraic_type": { - "Ref": 37 - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "item_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "category" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "description" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "quantity" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "rarity" - }, - "algebraic_type": { - "Ref": 38 - } - }, - { - "name": { - "some": "tags" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "stackable" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "stack_key" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "equipment_slot_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 39 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "source_kind" - }, - "algebraic_type": { - "Ref": 40 - } - }, - { - "name": { - "some": "source_reference_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "common" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "uncommon" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "rare" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "epic" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "legendary" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "weapon" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "armor" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "relic" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "storyReward" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "questReward" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "treasureReward" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "npcGift" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "npcTrade" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "combatDrop" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "forgeCraft" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "forgeReforge" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "manualPatch" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "slot_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "quantity" - }, - "algebraic_type": { - "U32": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "slot_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "slot_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "quest_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "signal" - }, - "algebraic_type": { - "Ref": 45 - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "hostileNpcDefeated" - }, - "algebraic_type": { - "Ref": 46 - } - }, - { - "name": { - "some": "treasureInspected" - }, - "algebraic_type": { - "Ref": 47 - } - }, - { - "name": { - "some": "npcSparCompleted" - }, - "algebraic_type": { - "Ref": 48 - } - }, - { - "name": { - "some": "npcTalkCompleted" - }, - "algebraic_type": { - "Ref": 49 - } - }, - { - "name": { - "some": "sceneReached" - }, - "algebraic_type": { - "Ref": 50 - } - }, - { - "name": { - "some": "itemDelivered" - }, - "algebraic_type": { - "Ref": 51 - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "scene_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "hostile_npc_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "scene_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "npc_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "npc_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "scene_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "npc_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "item_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "quantity" - }, - "algebraic_type": { - "U32": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "binding_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "asset_object_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "entity_kind" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "entity_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "slot" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "asset_kind" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "asset_object_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "bucket" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "object_key" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "access_policy" - }, - "algebraic_type": { - "Ref": 54 - } - }, - { - "name": { - "some": "content_type" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "content_length" - }, - "algebraic_type": { - "U64": [] - } - }, - { - "name": { - "some": "content_hash" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "version" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "source_job_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "entity_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "asset_kind" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "private" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "publicRead" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "task_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "reference_kind" - }, - "algebraic_type": { - "Ref": 16 - } - }, - { - "name": { - "some": "reference_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "label" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "identity_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "provider" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "provider_uid" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "provider_union_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "phone_e_164" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "display_name" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "avatar_url" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "snapshot_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "snapshot_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "battle_state_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "story_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "actor_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "chapter_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "target_npc_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "target_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "battle_mode" - }, - "algebraic_type": { - "Ref": 59 - } - }, - { - "name": { - "some": "status" - }, - "algebraic_type": { - "Ref": 60 - } - }, - { - "name": { - "some": "player_hp" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "player_max_hp" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "player_mana" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "player_max_mana" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "target_hp" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "target_max_hp" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "experience_reward" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "reward_items" - }, - "algebraic_type": { - "Array": { - "Ref": 61 - } - } - }, - { - "name": { - "some": "turn_index" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "last_action_function_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "last_action_text" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "last_result_text" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "last_damage_dealt" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "last_damage_taken" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "last_outcome" - }, - "algebraic_type": { - "Ref": 64 - } - }, - { - "name": { - "some": "version" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "fight" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "spar" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "ongoing" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "resolved" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "aborted" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "item_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "category" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "item_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "description" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "quantity" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "rarity" - }, - "algebraic_type": { - "Ref": 62 - } - }, - { - "name": { - "some": "tags" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "stackable" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "stack_key" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "equipment_slot_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 63 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "common" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "uncommon" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "rare" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "epic" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "legendary" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "weapon" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "armor" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "relic" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "ongoing" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "victory" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "sparComplete" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "escaped" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "story_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "actor_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "world_profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "initial_prompt" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "opening_summary" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "session" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 67 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "event" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 69 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "story_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "actor_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "world_profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "initial_prompt" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "opening_summary" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "latest_narrative_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "latest_choice_function_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "status" - }, - "algebraic_type": { - "Ref": 68 - } - }, - { - "name": { - "some": "version" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "active" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "completed" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "archived" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "event_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "story_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "event_kind" - }, - "algebraic_type": { - "Ref": 70 - } - }, - { - "name": { - "some": "narrative_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "choice_function_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "sessionStarted" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "storyContinued" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "message_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "role" - }, - "algebraic_type": { - "Ref": 72 - } - }, - { - "name": { - "some": "kind" - }, - "algebraic_type": { - "Ref": 73 - } - }, - { - "name": { - "some": "text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "user" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "assistant" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "system" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "chat" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "summary" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "actionResult" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "warning" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "slot_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "asset_kind" - }, - "algebraic_type": { - "Ref": 75 - } - }, - { - "name": { - "some": "level" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "motion_key" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "status" - }, - "algebraic_type": { - "Ref": 76 - } - }, - { - "name": { - "some": "asset_url" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "prompt_snapshot" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "levelMainImage" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "levelMotion" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "stageBackground" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "missing" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "ready" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "seed_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "current_turn" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "progress_percent" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "stage" - }, - "algebraic_type": { - "Ref": 78 - } - }, - { - "name": { - "some": "anchor_pack_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "draft_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "asset_coverage_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "last_assistant_reply" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "publish_ready" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "collectingAnchors" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "draftReady" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "assetRefining" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "readyToPublish" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "published" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "run_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "status" - }, - "algebraic_type": { - "Ref": 80 - } - }, - { - "name": { - "some": "snapshot_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "last_input_x" - }, - "algebraic_type": { - "F32": [] - } - }, - { - "name": { - "some": "last_input_y" - }, - "algebraic_type": { - "F32": [] - } - }, - { - "name": { - "some": "tick" - }, - "algebraic_type": { - "U64": [] - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "running" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "won" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "failed" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "binding_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "asset_object_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "entity_kind" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "entity_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "slot" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "asset_kind" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "record" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 83 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "binding_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "asset_object_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "entity_kind" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "entity_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "slot" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "asset_kind" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "task_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "completed_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "chapter_progression_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "chapter_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "chapter_index" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "total_chapters" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "entry_pseudo_level_millis" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "exit_pseudo_level_millis" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "entry_level" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "exit_level" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "planned_total_xp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "planned_quest_xp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "planned_hostile_xp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "actual_quest_xp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "actual_hostile_xp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "expected_hostile_defeat_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "actual_hostile_defeat_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "level_at_entry" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "level_at_exit" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "pace_band" - }, - "algebraic_type": { - "Ref": 33 - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "entries" - }, - "algebraic_type": { - "Array": { - "Ref": 88 - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "browse_history_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "world_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "subtitle" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "summary_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "cover_image_src" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "theme_mode" - }, - "algebraic_type": { - "Ref": 89 - } - }, - { - "name": { - "some": "author_display_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "visited_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "martial" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "arcane" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "machina" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "tide" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "rift" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "mythic" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "compiled_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "session" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 92 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "seed_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "current_turn" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "progress_percent" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "stage" - }, - "algebraic_type": { - "Ref": 78 - } - }, - { - "name": { - "some": "anchor_pack" - }, - "algebraic_type": { - "Ref": 93 - } - }, - { - "name": { - "some": "draft" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 96 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "asset_slots" - }, - "algebraic_type": { - "Array": { - "Ref": 100 - } - } - }, - { - "name": { - "some": "asset_coverage" - }, - "algebraic_type": { - "Ref": 101 - } - }, - { - "name": { - "some": "messages" - }, - "algebraic_type": { - "Array": { - "Ref": 102 - } - } - }, - { - "name": { - "some": "last_assistant_reply" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "publish_ready" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "gameplay_promise" - }, - "algebraic_type": { - "Ref": 94 - } - }, - { - "name": { - "some": "ecology_visual_theme" - }, - "algebraic_type": { - "Ref": 94 - } - }, - { - "name": { - "some": "growth_ladder" - }, - "algebraic_type": { - "Ref": 94 - } - }, - { - "name": { - "some": "risk_tempo" - }, - "algebraic_type": { - "Ref": 94 - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "key" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "label" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "value" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "status" - }, - "algebraic_type": { - "Ref": 95 - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "confirmed" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "inferred" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "missing" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "locked" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "title" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "subtitle" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "core_fun" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "ecology_theme" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "levels" - }, - "algebraic_type": { - "Array": { - "Ref": 97 - } - } - }, - { - "name": { - "some": "background" - }, - "algebraic_type": { - "Ref": 98 - } - }, - { - "name": { - "some": "runtime_params" - }, - "algebraic_type": { - "Ref": 99 - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "level" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "one_line_fantasy" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "silhouette_direction" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "size_ratio" - }, - "algebraic_type": { - "F32": [] - } - }, - { - "name": { - "some": "visual_prompt_seed" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "motion_prompt_seed" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "merge_source_level" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "prey_window" - }, - "algebraic_type": { - "Array": { - "U32": [] - } - } - }, - { - "name": { - "some": "threat_window" - }, - "algebraic_type": { - "Array": { - "U32": [] - } - } - }, - { - "name": { - "some": "is_final_level" - }, - "algebraic_type": { - "Bool": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "theme" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "color_mood" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "foreground_hints" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "midground_composition" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "background_depth" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "safe_play_area_hint" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "spawn_edge_hint" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "background_prompt_seed" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "level_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "merge_count_per_upgrade" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "spawn_target_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "leader_move_speed" - }, - "algebraic_type": { - "F32": [] - } - }, - { - "name": { - "some": "follower_catch_up_speed" - }, - "algebraic_type": { - "F32": [] - } - }, - { - "name": { - "some": "offscreen_cull_seconds" - }, - "algebraic_type": { - "F32": [] - } - }, - { - "name": { - "some": "prey_spawn_delta_levels" - }, - "algebraic_type": { - "Array": { - "U32": [] - } - } - }, - { - "name": { - "some": "threat_spawn_delta_levels" - }, - "algebraic_type": { - "Array": { - "U32": [] - } - } - }, - { - "name": { - "some": "win_level" - }, - "algebraic_type": { - "U32": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "slot_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "asset_kind" - }, - "algebraic_type": { - "Ref": 75 - } - }, - { - "name": { - "some": "level" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "motion_key" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "status" - }, - "algebraic_type": { - "Ref": 76 - } - }, - { - "name": { - "some": "asset_url" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "prompt_snapshot" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "level_main_image_ready_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "level_motion_ready_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "background_ready" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "required_level_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "publish_ready" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "blockers" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "message_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "role" - }, - "algebraic_type": { - "Ref": 72 - } - }, - { - "name": { - "some": "kind" - }, - "algebraic_type": { - "Ref": 73 - } - }, - { - "name": { - "some": "text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "draft_profile_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "legacy_result_profile_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "setting_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "author_display_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "record" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 105 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "world_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "subtitle" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "summary_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "theme_mode" - }, - "algebraic_type": { - "Ref": 106 - } - }, - { - "name": { - "some": "cover_image_src" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "playable_npc_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "landmark_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "author_display_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "compiled_profile_payload_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "martial" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "arcane" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "machina" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "tide" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "rift" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "mythic" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "compiled_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "session_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "task_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "stage_kind" - }, - "algebraic_type": { - "Ref": 21 - } - }, - { - "name": { - "some": "text_output" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "structured_payload_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "warning_messages" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "completed_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "task_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "completed_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "asset_object_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "bucket" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "object_key" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "access_policy" - }, - "algebraic_type": { - "Ref": 54 - } - }, - { - "name": { - "some": "content_type" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "content_length" - }, - "algebraic_type": { - "U64": [] - } - }, - { - "name": { - "some": "content_hash" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "version" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "source_job_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "entity_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "asset_kind" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "record" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 113 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "asset_object_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "bucket" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "object_key" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "access_policy" - }, - "algebraic_type": { - "Ref": 54 - } - }, - { - "name": { - "some": "content_type" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "content_length" - }, - "algebraic_type": { - "U64": [] - } - }, - { - "name": { - "some": "content_hash" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "version" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "source_job_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "entity_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "asset_kind" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "story_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "event_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "narrative_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "choice_function_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "task_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "task_kind" - }, - "algebraic_type": { - "Ref": 18 - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "request_label" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "source_module" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "source_entity_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "request_payload_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "stages" - }, - "algebraic_type": { - "Array": { - "Ref": 116 - } - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "stage_kind" - }, - "algebraic_type": { - "Ref": 21 - } - }, - { - "name": { - "some": "label" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "detail" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "order" - }, - "algebraic_type": { - "U32": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "battle_state_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "story_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "actor_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "chapter_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "target_npc_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "target_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "battle_mode" - }, - "algebraic_type": { - "Ref": 59 - } - }, - { - "name": { - "some": "player_hp" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "player_max_hp" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "player_mana" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "player_max_mana" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "target_hp" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "target_max_hp" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "experience_reward" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "reward_items" - }, - "algebraic_type": { - "Array": { - "Ref": 61 - } - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "snapshot" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 119 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "battle_state_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "story_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "actor_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "chapter_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "target_npc_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "target_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "battle_mode" - }, - "algebraic_type": { - "Ref": 59 - } - }, - { - "name": { - "some": "status" - }, - "algebraic_type": { - "Ref": 60 - } - }, - { - "name": { - "some": "player_hp" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "player_max_hp" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "player_mana" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "player_max_mana" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "target_hp" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "target_max_hp" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "experience_reward" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "reward_items" - }, - "algebraic_type": { - "Array": { - "Ref": 61 - } - } - }, - { - "name": { - "some": "turn_index" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "last_action_function_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "last_action_text" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "last_result_text" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "last_damage_dealt" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "last_damage_taken" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "last_outcome" - }, - "algebraic_type": { - "Ref": 64 - } - }, - { - "name": { - "some": "version" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "seed_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "welcome_message_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "welcome_message_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "seed_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "welcome_message_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "welcome_message_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "anchor_content_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "creator_intent_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "creator_intent_readiness_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "anchor_pack_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "lock_state_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "draft_profile_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "pending_clarifications_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "suggested_actions_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "recommended_replies_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "quality_findings_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "asset_coverage_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "checkpoints_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "session" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 123 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "seed_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "current_turn" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "progress_percent" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "stage" - }, - "algebraic_type": { - "Ref": 124 - } - }, - { - "name": { - "some": "focus_card_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "anchor_content_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "creator_intent_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "creator_intent_readiness_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "anchor_pack_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "lock_state_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "draft_profile_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "last_assistant_reply" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "publish_gate_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "result_preview_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "pending_clarifications_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "quality_findings_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "suggested_actions_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "recommended_replies_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "asset_coverage_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "checkpoints_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "supported_actions_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "messages" - }, - "algebraic_type": { - "Array": { - "Ref": 125 - } - } - }, - { - "name": { - "some": "draft_cards" - }, - "algebraic_type": { - "Array": { - "Ref": 128 - } - } - }, - { - "name": { - "some": "operations" - }, - "algebraic_type": { - "Array": { - "Ref": 132 - } - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "collectingIntent" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "clarifying" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "foundationReview" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "objectRefining" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "visualRefining" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "longTailReview" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "readyToPublish" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "published" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "error" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "message_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "role" - }, - "algebraic_type": { - "Ref": 126 - } - }, - { - "name": { - "some": "kind" - }, - "algebraic_type": { - "Ref": 127 - } - }, - { - "name": { - "some": "text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "related_operation_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "user" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "assistant" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "system" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "chat" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "clarification" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "summary" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "checkpoint" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "warning" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "actionResult" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "card_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "kind" - }, - "algebraic_type": { - "Ref": 129 - } - }, - { - "name": { - "some": "status" - }, - "algebraic_type": { - "Ref": 130 - } - }, - { - "name": { - "some": "title" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "subtitle" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "summary" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "linked_ids_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "warning_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "asset_status" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 131 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "asset_status_label" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "detail_payload_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "world" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "camp" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "faction" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "character" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "landmark" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "thread" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "chapter" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "sceneChapter" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "carrier" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "sidequestSeed" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "suggested" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "confirmed" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "locked" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "warning" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "missing" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "visualReady" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "animationsReady" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "complete" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "operation_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "operation_type" - }, - "algebraic_type": { - "Ref": 133 - } - }, - { - "name": { - "some": "status" - }, - "algebraic_type": { - "Ref": 134 - } - }, - { - "name": { - "some": "phase_label" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "phase_detail" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "progress" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "processMessage" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "draftFoundation" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "updateDraftCard" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "syncResultProfile" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "generateCharacters" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "generateLandmarks" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "deleteCharacters" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "deleteLandmarks" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "generateRoleAssets" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "syncRoleAssets" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "generateSceneAssets" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "syncSceneAssets" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "expandLongTail" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "publishWorld" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "revertCheckpoint" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "queued" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "running" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "completed" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "failed" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "seed_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "welcome_message_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "welcome_message_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "message_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "role" - }, - "algebraic_type": { - "Ref": 126 - } - }, - { - "name": { - "some": "kind" - }, - "algebraic_type": { - "Ref": 127 - } - }, - { - "name": { - "some": "text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "related_operation_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "operation_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "operation_type" - }, - "algebraic_type": { - "Ref": 133 - } - }, - { - "name": { - "some": "status" - }, - "algebraic_type": { - "Ref": 134 - } - }, - { - "name": { - "some": "phase_label" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "phase_detail" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "progress" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "seed_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "current_turn" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "progress_percent" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "stage" - }, - "algebraic_type": { - "Ref": 124 - } - }, - { - "name": { - "some": "focus_card_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "anchor_content_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "creator_intent_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "creator_intent_readiness_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "anchor_pack_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "lock_state_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "draft_profile_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "last_assistant_reply" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "publish_gate_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "result_preview_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "pending_clarifications_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "quality_findings_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "suggested_actions_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "recommended_replies_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "asset_coverage_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "checkpoints_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "card_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "kind" - }, - "algebraic_type": { - "Ref": 129 - } - }, - { - "name": { - "some": "status" - }, - "algebraic_type": { - "Ref": 130 - } - }, - { - "name": { - "some": "title" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "subtitle" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "summary" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "linked_ids_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "warning_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "asset_status" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 131 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "asset_status_label" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "detail_payload_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "public_work_code" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "author_public_user_code" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "author_display_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "world_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "subtitle" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "summary_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "cover_image_src" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "theme_mode" - }, - "algebraic_type": { - "Ref": 106 - } - }, - { - "name": { - "some": "playable_npc_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "landmark_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "published_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "public_work_code" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "author_public_user_code" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "source_agent_session_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "publication_status" - }, - "algebraic_type": { - "Ref": 142 - } - }, - { - "name": { - "some": "world_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "subtitle" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "summary_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "theme_mode" - }, - "algebraic_type": { - "Ref": 106 - } - }, - { - "name": { - "some": "cover_image_src" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "profile_payload_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "playable_npc_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "landmark_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "author_display_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "published_at" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "deleted_at" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "draft" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "published" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "generation_mode" - }, - "algebraic_type": { - "Ref": 144 - } - }, - { - "name": { - "some": "status" - }, - "algebraic_type": { - "Ref": 145 - } - }, - { - "name": { - "some": "setting_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "creator_intent_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "question_snapshot_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "result_payload_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "last_error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "fast" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "full" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "clarifying" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "readyToGenerate" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "generating" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "completed" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "generationError" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "items_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "deleted_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "entries" - }, - "algebraic_type": { - "Array": { - "Ref": 150 - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "public_work_code" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "author_public_user_code" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "source_agent_session_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "publication_status" - }, - "algebraic_type": { - "Ref": 142 - } - }, - { - "name": { - "some": "world_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "subtitle" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "summary_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "theme_mode" - }, - "algebraic_type": { - "Ref": 106 - } - }, - { - "name": { - "some": "cover_image_src" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "profile_payload_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "playable_npc_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "landmark_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "author_display_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "published_at_micros" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "deleted_at_micros" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "items_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "record" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 155 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "version" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "saved_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "bottom_tab" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "game_state_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "current_story_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "run_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "piece_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "target_row" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "target_col" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "dragged_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "operation_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "action" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "payload_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "submitted_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "operation" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 132 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "record" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 160 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "snapshot_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "task_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "failure_message" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "completed_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "assistant_message_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "assistant_reply_text" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "stage" - }, - "algebraic_type": { - "Ref": 78 - } - }, - { - "name": { - "some": "progress_percent" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "anchor_pack_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "operation_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "assistant_message_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "assistant_reply_text" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "phase_label" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "phase_detail" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "operation_status" - }, - "algebraic_type": { - "Ref": 134 - } - }, - { - "name": { - "some": "operation_progress" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "stage" - }, - "algebraic_type": { - "Ref": 124 - } - }, - { - "name": { - "some": "progress_percent" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "focus_card_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "anchor_content_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "creator_intent_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "creator_intent_readiness_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "anchor_pack_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "draft_profile_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "pending_clarifications_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "suggested_actions_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "recommended_replies_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "quality_findings_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "asset_coverage_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "operation" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 132 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "assistant_message_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "assistant_reply_text" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "stage" - }, - "algebraic_type": { - "Ref": 166 - } - }, - { - "name": { - "some": "progress_percent" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "anchor_pack_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "collectingAnchors" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "draftReady" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "imageRefining" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "readyToPublish" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "published" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "asset_kind" - }, - "algebraic_type": { - "Ref": 75 - } - }, - { - "name": { - "some": "level" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "motion_key" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "asset_url" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "generated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "battle_state_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "run_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "run" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 171 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "run_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "status" - }, - "algebraic_type": { - "Ref": 80 - } - }, - { - "name": { - "some": "tick" - }, - "algebraic_type": { - "U64": [] - } - }, - { - "name": { - "some": "player_level" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "win_level" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "leader_entity_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "owned_entities" - }, - "algebraic_type": { - "Array": { - "Ref": 172 - } - } - }, - { - "name": { - "some": "wild_entities" - }, - "algebraic_type": { - "Array": { - "Ref": 172 - } - } - }, - { - "name": { - "some": "camera_center" - }, - "algebraic_type": { - "Ref": 173 - } - }, - { - "name": { - "some": "last_input" - }, - "algebraic_type": { - "Ref": 173 - } - }, - { - "name": { - "some": "event_log" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "entity_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "level" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "position" - }, - "algebraic_type": { - "Ref": 173 - } - }, - { - "name": { - "some": "radius" - }, - "algebraic_type": { - "F32": [] - } - }, - { - "name": { - "some": "offscreen_seconds" - }, - "algebraic_type": { - "F32": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "x" - }, - "algebraic_type": { - "F32": [] - } - }, - { - "name": { - "some": "y" - }, - "algebraic_type": { - "F32": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "chapter_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "card_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "card" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 178 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "card_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "kind" - }, - "algebraic_type": { - "Ref": 129 - } - }, - { - "name": { - "some": "title" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "sections" - }, - "algebraic_type": { - "Array": { - "Ref": 179 - } - } - }, - { - "name": { - "some": "linked_ids_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "locked" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "editable" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "editable_section_ids_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "warning_messages_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "asset_status" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 131 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "asset_status_label" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "section_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "label" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "value" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "operation_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "entry" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 150 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "gallery_entry" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 184 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "public_work_code" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "author_public_user_code" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "author_display_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "world_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "subtitle" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "summary_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "cover_image_src" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "theme_mode" - }, - "algebraic_type": { - "Ref": 106 - } - }, - { - "name": { - "some": "playable_npc_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "landmark_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "published_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "public_work_code" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "record" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 189 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "level" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "current_level_xp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "total_xp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "xp_to_next_level" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "pending_level_ups" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "last_granted_source" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 190 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "quest" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "hostileNpc" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "record" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 193 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "wallet_balance" - }, - "algebraic_type": { - "U64": [] - } - }, - { - "name": { - "some": "total_play_time_ms" - }, - "algebraic_type": { - "U64": [] - } - }, - { - "name": { - "some": "played_world_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "record" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 196 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "total_play_time_ms" - }, - "algebraic_type": { - "U64": [] - } - }, - { - "name": { - "some": "played_works" - }, - "algebraic_type": { - "Array": { - "Ref": 197 - } - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "played_world_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "world_key" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "world_type" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "world_title" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "world_subtitle" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "first_played_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "last_played_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "last_observed_play_time_ms" - }, - "algebraic_type": { - "U64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "item_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "run_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "actor_user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "snapshot" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 204 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "actor_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "backpack_items" - }, - "algebraic_type": { - "Array": { - "Ref": 205 - } - } - }, - { - "name": { - "some": "equipment_items" - }, - "algebraic_type": { - "Array": { - "Ref": 205 - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "slot_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "story_session_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "actor_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "container_kind" - }, - "algebraic_type": { - "Ref": 206 - } - }, - { - "name": { - "some": "slot_key" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "item_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "category" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "description" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "quantity" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "rarity" - }, - "algebraic_type": { - "Ref": 38 - } - }, - { - "name": { - "some": "tags" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "stackable" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "stack_key" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "equipment_slot_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 39 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "source_kind" - }, - "algebraic_type": { - "Ref": 40 - } - }, - { - "name": { - "some": "source_reference_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "backpack" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "equipment" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "record" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 209 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "music_volume" - }, - "algebraic_type": { - "F32": [] - } - }, - { - "name": { - "some": "platform_theme" - }, - "algebraic_type": { - "Ref": 210 - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "light" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "dark" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "story_session_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "session" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 67 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "events" - }, - "algebraic_type": { - "Array": { - "Ref": 69 - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "amount" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "source" - }, - "algebraic_type": { - "Ref": 190 - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "record" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 216 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "imported_user_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "imported_identity_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "imported_refresh_session_count" - }, - "algebraic_type": { - "U32": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "slot_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "story_session_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "actor_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "container_kind" - }, - "algebraic_type": { - "Ref": 206 - } - }, - { - "name": { - "some": "slot_key" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "item_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "category" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "description" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "quantity" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "rarity" - }, - "algebraic_type": { - "Ref": 38 - } - }, - { - "name": { - "some": "tags" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "stackable" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "stack_key" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "equipment_slot_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 39 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "source_kind" - }, - "algebraic_type": { - "Ref": 40 - } - }, - { - "name": { - "some": "source_reference_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "entries" - }, - "algebraic_type": { - "Array": { - "Ref": 184 - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "items" - }, - "algebraic_type": { - "Array": { - "Ref": 223 - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "work_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "source_type" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "status" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "title" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "subtitle" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "summary" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "cover_image_src" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "cover_render_mode" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "cover_character_image_srcs_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "published_at_micros" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "stage" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 124 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "stage_label" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "playable_npc_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "landmark_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "role_visual_ready_count" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "role_animation_ready_count" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "role_asset_summary_label" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "can_resume" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "can_enter_world" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "blocker_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "publish_ready" - }, - "algebraic_type": { - "Bool": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "entries" - }, - "algebraic_type": { - "Array": { - "Ref": 227 - } - } - }, - { - "name": { - "some": "record" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 227 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "current_snapshot" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 155 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "archive_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "world_key" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "world_type" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "world_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "subtitle" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "summary_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "cover_image_src" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "saved_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "bottom_tab" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "game_state_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "current_story_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "entries" - }, - "algebraic_type": { - "Array": { - "Ref": 230 - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "wallet_ledger_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "amount_delta" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "balance_after" - }, - "algebraic_type": { - "U64": [] - } - }, - { - "name": { - "some": "source_type" - }, - "algebraic_type": { - "Ref": 231 - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "snapshotSync" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "npc_state_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "npc_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "npc_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "affinity" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "relation_state" - }, - "algebraic_type": { - "Ref": 234 - } - }, - { - "name": { - "some": "help_used" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "chatted_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "gifts_given" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "recruited" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "trade_stock_signature" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "revealed_facts" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "known_attribute_rumors" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "first_meaningful_contact_resolved" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "seen_backstory_chapter_ids" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "stance_profile" - }, - "algebraic_type": { - "Ref": 236 - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "affinity" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "stance" - }, - "algebraic_type": { - "Ref": 235 - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "hostile" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "guarded" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "neutral" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "cooperative" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "bonded" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "trust" - }, - "algebraic_type": { - "U8": [] - } - }, - { - "name": { - "some": "warmth" - }, - "algebraic_type": { - "U8": [] - } - }, - { - "name": { - "some": "ideological_fit" - }, - "algebraic_type": { - "U8": [] - } - }, - { - "name": { - "some": "fear_or_guard" - }, - "algebraic_type": { - "U8": [] - } - }, - { - "name": { - "some": "loyalty" - }, - "algebraic_type": { - "U8": [] - } - }, - { - "name": { - "some": "current_conflict_tag" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "recent_approvals" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "recent_disapprovals" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "level" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "current_level_xp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "total_xp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "xp_to_next_level" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "pending_level_ups" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "last_granted_source" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 190 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "wallet_balance" - }, - "algebraic_type": { - "U64": [] - } - }, - { - "name": { - "some": "total_play_time_ms" - }, - "algebraic_type": { - "U64": [] - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "played_world_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "world_key" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "world_type" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "world_title" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "world_subtitle" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "first_played_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "last_played_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "last_observed_play_time_ms" - }, - "algebraic_type": { - "U64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "archive_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "world_key" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "world_type" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "world_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "subtitle" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "summary_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "cover_image_src" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "saved_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "bottom_tab" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "game_state_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "current_story_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "wallet_ledger_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "amount_delta" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "balance_after" - }, - "algebraic_type": { - "U64": [] - } - }, - { - "name": { - "some": "source_type" - }, - "algebraic_type": { - "Ref": 231 - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "published_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "public_work_code" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "author_public_user_code" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "author_display_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "published_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "public_work_code" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "author_public_user_code" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "draft_profile_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "legacy_result_profile_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "setting_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "author_display_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "published_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "compiled_record" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 105 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "entry" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 150 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "gallery_entry" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 184 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "session_stage" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 124 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "work_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "author_display_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "level_name" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "summary" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "theme_tags" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "published_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "message_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "role" - }, - "algebraic_type": { - "Ref": 248 - } - }, - { - "name": { - "some": "kind" - }, - "algebraic_type": { - "Ref": 249 - } - }, - { - "name": { - "some": "text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "user" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "assistant" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "system" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "chat" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "summary" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "actionResult" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "warning" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "seed_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "current_turn" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "progress_percent" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "stage" - }, - "algebraic_type": { - "Ref": 166 - } - }, - { - "name": { - "some": "anchor_pack_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "draft_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "last_assistant_reply" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "published_profile_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "run_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "entry_profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "current_profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "cleared_level_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "current_level_index" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "current_grid_size" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "played_profile_ids_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "previous_level_tags_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "snapshot_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "work_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "source_session_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "author_display_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "level_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "summary" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "theme_tags_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "cover_image_src" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "cover_asset_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "publication_status" - }, - "algebraic_type": { - "Ref": 253 - } - }, - { - "name": { - "some": "play_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "anchor_pack_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "publish_ready" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "published_at" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "draft" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "published" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "log_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "quest_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "actor_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "event_kind" - }, - "algebraic_type": { - "Ref": 255 - } - }, - { - "name": { - "some": "status_after" - }, - "algebraic_type": { - "Ref": 1 - } - }, - { - "name": { - "some": "signal_kind" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 256 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "signal" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 45 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "step_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "step_progress" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "accepted" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "progressed" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "completed" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "completionAcknowledged" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "turnedIn" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "hostileNpcDefeated" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "treasureInspected" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "npcSparCompleted" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "npcTalkCompleted" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "sceneReached" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "itemDelivered" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "quest_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "story_session_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "actor_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "issuer_npc_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "issuer_npc_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "scene_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "chapter_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "act_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "thread_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "contract_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "title" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "description" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "summary" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "objective" - }, - "algebraic_type": { - "Ref": 258 - } - }, - { - "name": { - "some": "progress" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "status" - }, - "algebraic_type": { - "Ref": 1 - } - }, - { - "name": { - "some": "completion_notified" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "reward" - }, - "algebraic_type": { - "Ref": 2 - } - }, - { - "name": { - "some": "reward_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "narrative_binding" - }, - "algebraic_type": { - "Ref": 7 - } - }, - { - "name": { - "some": "steps" - }, - "algebraic_type": { - "Array": { - "Ref": 10 - } - } - }, - { - "name": { - "some": "active_step_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "visible_stage" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "hidden_flags" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "discovered_fact_ids" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "related_carrier_ids" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "consequence_ids" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "completed_at" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "turned_in_at" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "kind" - }, - "algebraic_type": { - "Ref": 11 - } - }, - { - "name": { - "some": "target_hostile_npc_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "target_npc_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "target_scene_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "target_item_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "required_count" - }, - "algebraic_type": { - "U32": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "refresh_token_hash" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "issued_by_provider" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "client_info_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "expires_at" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "revoked_at" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "last_seen_at" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "battle_state_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "function_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "action_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "base_damage" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "mana_cost" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "heal" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "mana_restore" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "counter_multiplier_basis_points" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "result" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 262 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "snapshot" - }, - "algebraic_type": { - "Ref": 119 - } - }, - { - "name": { - "some": "damage_dealt" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "damage_taken" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "outcome" - }, - "algebraic_type": { - "Ref": 64 - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "npc_interaction" - }, - "algebraic_type": { - "Ref": 264 - } - }, - { - "name": { - "some": "story_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "actor_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "battle_state_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "player_hp" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "player_max_hp" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "player_mana" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "player_max_mana" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "target_hp" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "target_max_hp" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "experience_reward" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "reward_items" - }, - "algebraic_type": { - "Array": { - "Ref": 61 - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "npc_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "npc_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "interaction_function_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "release_npc_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "result" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 266 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "interaction" - }, - "algebraic_type": { - "Ref": 267 - } - }, - { - "name": { - "some": "battle_state" - }, - "algebraic_type": { - "Ref": 119 - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "npc_state" - }, - "algebraic_type": { - "Ref": 268 - } - }, - { - "name": { - "some": "interaction_status" - }, - "algebraic_type": { - "Ref": 269 - } - }, - { - "name": { - "some": "action_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "result_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "story_text" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "battle_mode" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 270 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "encounter_closed" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "affinity_changed" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "previous_affinity" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "next_affinity" - }, - "algebraic_type": { - "I32": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "npc_state_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "npc_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "npc_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "affinity" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "relation_state" - }, - "algebraic_type": { - "Ref": 234 - } - }, - { - "name": { - "some": "help_used" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "chatted_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "gifts_given" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "recruited" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "trade_stock_signature" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "revealed_facts" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "known_attribute_rumors" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "first_meaningful_contact_resolved" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "seen_backstory_chapter_ids" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "stance_profile" - }, - "algebraic_type": { - "Ref": 236 - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "previewed" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "dialogue" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "resolved" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "recruited" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "battlePending" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "left" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "fight" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "spar" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "result" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 267 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "npc_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "npc_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "action_kind" - }, - "algebraic_type": { - "Ref": 273 - } - }, - { - "name": { - "some": "affinity_gain_override" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "note" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "chat" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "help" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "gift" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "recruit" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "questAccept" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "record" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 268 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "treasure_record_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "story_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "actor_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "encounter_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "encounter_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "scene_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "scene_name" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "action" - }, - "algebraic_type": { - "Ref": 276 - } - }, - { - "name": { - "some": "reward_items" - }, - "algebraic_type": { - "Array": { - "Ref": 61 - } - } - }, - { - "name": { - "some": "reward_hp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "reward_mana" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "reward_currency" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "story_hint" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Sum": { - "variants": [ - { - "name": { - "some": "inspect" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "leave" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - }, - { - "name": { - "some": "secure" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "ok" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "record" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 278 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "treasure_record_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "story_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "actor_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "encounter_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "encounter_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "scene_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "scene_name" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "action" - }, - "algebraic_type": { - "Ref": 276 - } - }, - { - "name": { - "some": "reward_items" - }, - "algebraic_type": { - "Array": { - "Ref": 61 - } - } - }, - { - "name": { - "some": "reward_hp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "reward_mana" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "reward_currency" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "story_hint" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "world_key" - }, - "algebraic_type": { - "String": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "music_volume" - }, - "algebraic_type": { - "F32": [] - } - }, - { - "name": { - "some": "platform_theme" - }, - "algebraic_type": { - "Ref": 210 - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "version" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "saved_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "bottom_tab" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "game_state_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "current_story_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "candidates_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "saved_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "candidate_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "selected_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "task_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "started_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "task_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "stage_kind" - }, - "algebraic_type": { - "Ref": 21 - } - }, - { - "name": { - "some": "started_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "run_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "started_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "run_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "started_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "event_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "story_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "event_kind" - }, - "algebraic_type": { - "Ref": 70 - } - }, - { - "name": { - "some": "narrative_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "choice_function_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "story_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "actor_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "world_profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "initial_prompt" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "opening_summary" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "latest_narrative_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "latest_choice_function_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "status" - }, - "algebraic_type": { - "Ref": 68 - } - }, - { - "name": { - "some": "version" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "run_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "input_x" - }, - "algebraic_type": { - "F32": [] - } - }, - { - "name": { - "some": "input_y" - }, - "algebraic_type": { - "F32": [] - } - }, - { - "name": { - "some": "submitted_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "user_message_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "user_message_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "assistant_message_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "submitted_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "user_message_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "user_message_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "operation_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "submitted_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "user_message_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "user_message_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "submitted_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "run_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "first_piece_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "second_piece_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "swapped_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "treasure_record_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "story_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "actor_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "encounter_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "encounter_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "scene_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "scene_name" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "action" - }, - "algebraic_type": { - "Ref": 276 - } - }, - { - "name": { - "some": "reward_items" - }, - "algebraic_type": { - "Array": { - "Ref": 61 - } - } - }, - { - "name": { - "some": "reward_hp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "reward_mana" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "reward_currency" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "story_hint" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "quest_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "turned_in_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "author_display_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "level_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "summary" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "theme_tags" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "cover_image_src" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "cover_asset_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "snapshot_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "chapter_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "chapter_index" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "total_chapters" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "entry_pseudo_level_millis" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "exit_pseudo_level_millis" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "entry_level" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "exit_level" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "planned_total_xp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "planned_quest_xp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "planned_hostile_xp" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "expected_hostile_defeat_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "level_at_entry" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "pace_band" - }, - "algebraic_type": { - "Ref": 33 - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "operation_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "operation_type" - }, - "algebraic_type": { - "Ref": 133 - } - }, - { - "name": { - "some": "operation_status" - }, - "algebraic_type": { - "Ref": 134 - } - }, - { - "name": { - "some": "phase_label" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "phase_detail" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "operation_progress" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "error_message" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "public_work_code" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "author_public_user_code" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "source_agent_session_id" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "world_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "subtitle" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "summary_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "theme_mode" - }, - "algebraic_type": { - "Ref": 106 - } - }, - { - "name": { - "some": "cover_image_src" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "profile_payload_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "playable_npc_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "landmark_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "author_display_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "runtime_session_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "npc_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "npc_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "affinity" - }, - "algebraic_type": { - "I32": [] - } - }, - { - "name": { - "some": "help_used" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "chatted_count" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "gifts_given" - }, - "algebraic_type": { - "U32": [] - } - }, - { - "name": { - "some": "recruited" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "trade_stock_signature" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "revealed_facts" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "known_attribute_rumors" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "first_meaningful_contact_resolved" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "seen_backstory_chapter_ids" - }, - "algebraic_type": { - "Array": { - "String": [] - } - } - }, - { - "name": { - "some": "stance_profile" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "Ref": 236 - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "entries" - }, - "algebraic_type": { - "Array": { - "Ref": 305 - } - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "world_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "subtitle" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "summary_text" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "cover_image_src" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "theme_mode" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "author_display_name" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "visited_at" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "music_volume" - }, - "algebraic_type": { - "F32": [] - } - }, - { - "name": { - "some": "platform_theme" - }, - "algebraic_type": { - "Ref": 210 - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "saved_at_micros" - }, - "algebraic_type": { - "I64": [] - } - }, - { - "name": { - "some": "bottom_tab" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "game_state_json" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "current_story_json" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at_micros" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "public_user_code" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "username" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "display_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "phone_number_masked" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "phone_number_e_164" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "login_method" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "binding_status" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "wechat_bound" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "password_hash" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "password_login_enabled" - }, - "algebraic_type": { - "Bool": [] - } - }, - { - "name": { - "some": "token_version" - }, - "algebraic_type": { - "U64": [] - } - } - ] - } - }, - { - "Product": { - "elements": [ - { - "name": { - "some": "browse_history_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "owner_user_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "profile_id" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "world_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "subtitle" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "summary_text" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "cover_image_src" - }, - "algebraic_type": { - "Sum": { - "variants": [ - { - "name": { - "some": "some" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "none" - }, - "algebraic_type": { - "Product": { - "elements": [] - } - } - } - ] - } - } - }, - { - "name": { - "some": "theme_mode" - }, - "algebraic_type": { - "Ref": 89 - } - }, - { - "name": { - "some": "author_display_name" - }, - "algebraic_type": { - "String": [] - } - }, - { - "name": { - "some": "visited_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "created_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - }, - { - "name": { - "some": "updated_at" - }, - "algebraic_type": { - "Product": { - "elements": [ - { - "name": { - "some": "__timestamp_micros_since_unix_epoch__" - }, - "algebraic_type": { - "I64": [] - } - } - ] - } - } - } - ] - } - } - ] - }, - "tables": [ - { - "name": "ai_result_reference", - "product_type_ref": 15, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "ai_result_reference_result_reference_row_id_idx_btree" - }, - "accessor_name": { - "some": "result_reference_row_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "ai_result_reference_task_id_idx_btree" - }, - "accessor_name": { - "some": "by_ai_result_reference_task_id" - }, - "algorithm": { - "BTree": [ - 2 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "ai_result_reference_result_reference_row_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "ai_task", - "product_type_ref": 17, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "ai_task_owner_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_ai_task_owner_user_id" - }, - "algorithm": { - "BTree": [ - 2 - ] - } - }, - { - "name": { - "some": "ai_task_status_idx_btree" - }, - "accessor_name": { - "some": "by_ai_task_status" - }, - "algorithm": { - "BTree": [ - 7 - ] - } - }, - { - "name": { - "some": "ai_task_task_id_idx_btree" - }, - "accessor_name": { - "some": "task_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "ai_task_task_kind_idx_btree" - }, - "accessor_name": { - "some": "by_ai_task_kind" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "ai_task_task_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "ai_task_stage", - "product_type_ref": 20, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "ai_task_stage_task_id_idx_btree" - }, - "accessor_name": { - "some": "by_ai_task_stage_task_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - }, - { - "name": { - "some": "ai_task_stage_task_id_stage_order_idx_btree" - }, - "accessor_name": { - "some": "by_ai_task_stage_task_order" - }, - "algorithm": { - "BTree": [ - 1, - 5 - ] - } - }, - { - "name": { - "some": "ai_task_stage_task_stage_id_idx_btree" - }, - "accessor_name": { - "some": "task_stage_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "ai_task_stage_task_stage_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "ai_text_chunk", - "product_type_ref": 23, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "ai_text_chunk_task_id_idx_btree" - }, - "accessor_name": { - "some": "by_ai_text_chunk_task_id" - }, - "algorithm": { - "BTree": [ - 2 - ] - } - }, - { - "name": { - "some": "ai_text_chunk_task_id_stage_kind_sequence_idx_btree" - }, - "accessor_name": { - "some": "by_ai_text_chunk_task_stage_sequence" - }, - "algorithm": { - "BTree": [ - 2, - 3, - 4 - ] - } - }, - { - "name": { - "some": "ai_text_chunk_text_chunk_row_id_idx_btree" - }, - "accessor_name": { - "some": "text_chunk_row_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "ai_text_chunk_text_chunk_row_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "asset_entity_binding", - "product_type_ref": 52, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "asset_entity_binding_asset_object_id_idx_btree" - }, - "accessor_name": { - "some": "by_asset_object_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - }, - { - "name": { - "some": "asset_entity_binding_binding_id_idx_btree" - }, - "accessor_name": { - "some": "binding_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "asset_entity_binding_entity_kind_entity_id_slot_idx_btree" - }, - "accessor_name": { - "some": "by_entity_slot" - }, - "algorithm": { - "BTree": [ - 2, - 3, - 4 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "asset_entity_binding_binding_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "asset_object", - "product_type_ref": 53, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "asset_object_asset_kind_idx_btree" - }, - "accessor_name": { - "some": "asset_kind" - }, - "algorithm": { - "BTree": [ - 12 - ] - } - }, - { - "name": { - "some": "asset_object_asset_object_id_idx_btree" - }, - "accessor_name": { - "some": "asset_object_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "asset_object_bucket_object_key_idx_btree" - }, - "accessor_name": { - "some": "by_bucket_object_key" - }, - "algorithm": { - "BTree": [ - 1, - 2 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "asset_object_asset_object_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "auth_identity", - "product_type_ref": 56, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "auth_identity_identity_id_idx_btree" - }, - "accessor_name": { - "some": "identity_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "auth_identity_provider_provider_uid_idx_btree" - }, - "accessor_name": { - "some": "by_auth_identity_provider_uid" - }, - "algorithm": { - "BTree": [ - 2, - 3 - ] - } - }, - { - "name": { - "some": "auth_identity_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_auth_identity_user_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "auth_identity_identity_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "auth_store_snapshot", - "product_type_ref": 57, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "auth_store_snapshot_snapshot_id_idx_btree" - }, - "accessor_name": { - "some": "snapshot_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "auth_store_snapshot_snapshot_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "battle_state", - "product_type_ref": 58, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "battle_state_actor_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_battle_actor_user_id" - }, - "algorithm": { - "BTree": [ - 3 - ] - } - }, - { - "name": { - "some": "battle_state_battle_state_id_idx_btree" - }, - "accessor_name": { - "some": "battle_state_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "battle_state_runtime_session_id_idx_btree" - }, - "accessor_name": { - "some": "by_battle_runtime_session_id" - }, - "algorithm": { - "BTree": [ - 2 - ] - } - }, - { - "name": { - "some": "battle_state_story_session_id_idx_btree" - }, - "accessor_name": { - "some": "by_battle_story_session_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "battle_state_battle_state_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "big_fish_agent_message", - "product_type_ref": 71, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "big_fish_agent_message_message_id_idx_btree" - }, - "accessor_name": { - "some": "message_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "big_fish_agent_message_session_id_idx_btree" - }, - "accessor_name": { - "some": "by_big_fish_message_session_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "big_fish_agent_message_message_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "big_fish_asset_slot", - "product_type_ref": 74, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "big_fish_asset_slot_session_id_idx_btree" - }, - "accessor_name": { - "some": "by_big_fish_asset_session_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - }, - { - "name": { - "some": "big_fish_asset_slot_slot_id_idx_btree" - }, - "accessor_name": { - "some": "slot_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "big_fish_asset_slot_slot_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "big_fish_creation_session", - "product_type_ref": 77, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "big_fish_creation_session_owner_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_big_fish_session_owner_user_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - }, - { - "name": { - "some": "big_fish_creation_session_session_id_idx_btree" - }, - "accessor_name": { - "some": "session_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "big_fish_creation_session_session_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "big_fish_runtime_run", - "product_type_ref": 79, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "big_fish_runtime_run_owner_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_big_fish_run_owner_user_id" - }, - "algorithm": { - "BTree": [ - 2 - ] - } - }, - { - "name": { - "some": "big_fish_runtime_run_run_id_idx_btree" - }, - "accessor_name": { - "some": "run_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "big_fish_runtime_run_session_id_idx_btree" - }, - "accessor_name": { - "some": "by_big_fish_run_session_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "big_fish_runtime_run_run_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "chapter_progression", - "product_type_ref": 85, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "chapter_progression_chapter_id_idx_btree" - }, - "accessor_name": { - "some": "by_chapter_progression_chapter_id" - }, - "algorithm": { - "BTree": [ - 2 - ] - } - }, - { - "name": { - "some": "chapter_progression_chapter_progression_id_idx_btree" - }, - "accessor_name": { - "some": "chapter_progression_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "chapter_progression_user_id_chapter_id_idx_btree" - }, - "accessor_name": { - "some": "by_chapter_progression_user_chapter" - }, - "algorithm": { - "BTree": [ - 1, - 2 - ] - } - }, - { - "name": { - "some": "chapter_progression_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_chapter_progression_user_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "chapter_progression_chapter_progression_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "custom_world_agent_message", - "product_type_ref": 136, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "custom_world_agent_message_message_id_idx_btree" - }, - "accessor_name": { - "some": "message_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "custom_world_agent_message_session_id_idx_btree" - }, - "accessor_name": { - "some": "by_custom_world_agent_message_session_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "custom_world_agent_message_message_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "custom_world_agent_operation", - "product_type_ref": 137, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "custom_world_agent_operation_operation_id_idx_btree" - }, - "accessor_name": { - "some": "operation_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "custom_world_agent_operation_session_id_idx_btree" - }, - "accessor_name": { - "some": "by_custom_world_agent_operation_session_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "custom_world_agent_operation_operation_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "custom_world_agent_session", - "product_type_ref": 138, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "custom_world_agent_session_owner_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_custom_world_agent_session_owner_user_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - }, - { - "name": { - "some": "custom_world_agent_session_session_id_idx_btree" - }, - "accessor_name": { - "some": "session_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "custom_world_agent_session_stage_idx_btree" - }, - "accessor_name": { - "some": "by_custom_world_agent_session_stage" - }, - "algorithm": { - "BTree": [ - 5 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "custom_world_agent_session_session_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "custom_world_draft_card", - "product_type_ref": 139, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "custom_world_draft_card_card_id_idx_btree" - }, - "accessor_name": { - "some": "card_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "custom_world_draft_card_kind_idx_btree" - }, - "accessor_name": { - "some": "by_custom_world_draft_card_kind" - }, - "algorithm": { - "BTree": [ - 2 - ] - } - }, - { - "name": { - "some": "custom_world_draft_card_session_id_idx_btree" - }, - "accessor_name": { - "some": "by_custom_world_draft_card_session_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "custom_world_draft_card_card_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "custom_world_gallery_entry", - "product_type_ref": 140, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "custom_world_gallery_entry_owner_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_custom_world_gallery_owner_user_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - }, - { - "name": { - "some": "custom_world_gallery_entry_profile_id_idx_btree" - }, - "accessor_name": { - "some": "profile_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "custom_world_gallery_entry_public_work_code_idx_btree" - }, - "accessor_name": { - "some": "by_custom_world_gallery_public_work_code" - }, - "algorithm": { - "BTree": [ - 2 - ] - } - }, - { - "name": { - "some": "custom_world_gallery_entry_theme_mode_idx_btree" - }, - "accessor_name": { - "some": "by_custom_world_gallery_theme_mode" - }, - "algorithm": { - "BTree": [ - 9 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "custom_world_gallery_entry_profile_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Public": [] - } - }, - { - "name": "custom_world_profile", - "product_type_ref": 141, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "custom_world_profile_owner_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_custom_world_profile_owner_user_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - }, - { - "name": { - "some": "custom_world_profile_profile_id_idx_btree" - }, - "accessor_name": { - "some": "profile_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "custom_world_profile_publication_status_idx_btree" - }, - "accessor_name": { - "some": "by_custom_world_profile_publication_status" - }, - "algorithm": { - "BTree": [ - 5 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "custom_world_profile_profile_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "custom_world_session", - "product_type_ref": 143, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "custom_world_session_owner_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_custom_world_session_owner_user_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - }, - { - "name": { - "some": "custom_world_session_session_id_idx_btree" - }, - "accessor_name": { - "some": "session_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "custom_world_session_session_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "inventory_slot", - "product_type_ref": 217, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "inventory_slot_actor_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_inventory_actor_user_id" - }, - "algorithm": { - "BTree": [ - 3 - ] - } - }, - { - "name": { - "some": "inventory_slot_container_kind_slot_key_idx_btree" - }, - "accessor_name": { - "some": "by_inventory_container_slot" - }, - "algorithm": { - "BTree": [ - 4, - 5 - ] - } - }, - { - "name": { - "some": "inventory_slot_item_id_idx_btree" - }, - "accessor_name": { - "some": "by_inventory_item_id" - }, - "algorithm": { - "BTree": [ - 6 - ] - } - }, - { - "name": { - "some": "inventory_slot_runtime_session_id_idx_btree" - }, - "accessor_name": { - "some": "by_inventory_runtime_session_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - }, - { - "name": { - "some": "inventory_slot_slot_id_idx_btree" - }, - "accessor_name": { - "some": "slot_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "inventory_slot_slot_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "npc_state", - "product_type_ref": 233, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "npc_state_npc_id_idx_btree" - }, - "accessor_name": { - "some": "by_npc_id" - }, - "algorithm": { - "BTree": [ - 2 - ] - } - }, - { - "name": { - "some": "npc_state_npc_state_id_idx_btree" - }, - "accessor_name": { - "some": "npc_state_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "npc_state_runtime_session_id_idx_btree" - }, - "accessor_name": { - "some": "by_runtime_session_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - }, - { - "name": { - "some": "npc_state_runtime_session_id_npc_id_idx_btree" - }, - "accessor_name": { - "some": "by_runtime_session_npc" - }, - "algorithm": { - "BTree": [ - 1, - 2 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "npc_state_npc_state_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "player_progression", - "product_type_ref": 237, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "player_progression_user_id_idx_btree" - }, - "accessor_name": { - "some": "user_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "player_progression_user_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "profile_dashboard_state", - "product_type_ref": 238, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "profile_dashboard_state_user_id_idx_btree" - }, - "accessor_name": { - "some": "user_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "profile_dashboard_state_user_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "profile_played_world", - "product_type_ref": 239, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "profile_played_world_played_world_id_idx_btree" - }, - "accessor_name": { - "some": "played_world_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "profile_played_world_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_profile_played_world_user_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - }, - { - "name": { - "some": "profile_played_world_user_id_last_played_at_idx_btree" - }, - "accessor_name": { - "some": "by_profile_played_world_user_last_played_at" - }, - "algorithm": { - "BTree": [ - 1, - 9 - ] - } - }, - { - "name": { - "some": "profile_played_world_user_id_world_key_idx_btree" - }, - "accessor_name": { - "some": "by_profile_played_world_user_world_key" - }, - "algorithm": { - "BTree": [ - 1, - 2 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "profile_played_world_played_world_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "profile_save_archive", - "product_type_ref": 240, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "profile_save_archive_archive_id_idx_btree" - }, - "accessor_name": { - "some": "archive_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "profile_save_archive_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_profile_save_archive_user_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - }, - { - "name": { - "some": "profile_save_archive_user_id_saved_at_idx_btree" - }, - "accessor_name": { - "some": "by_profile_save_archive_user_saved_at" - }, - "algorithm": { - "BTree": [ - 1, - 10 - ] - } - }, - { - "name": { - "some": "profile_save_archive_user_id_world_key_idx_btree" - }, - "accessor_name": { - "some": "by_profile_save_archive_user_world_key" - }, - "algorithm": { - "BTree": [ - 1, - 2 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "profile_save_archive_archive_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "profile_wallet_ledger", - "product_type_ref": 241, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "profile_wallet_ledger_user_id_created_at_idx_btree" - }, - "accessor_name": { - "some": "by_profile_wallet_ledger_user_created_at" - }, - "algorithm": { - "BTree": [ - 1, - 5 - ] - } - }, - { - "name": { - "some": "profile_wallet_ledger_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_profile_wallet_ledger_user_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - }, - { - "name": { - "some": "profile_wallet_ledger_wallet_ledger_id_idx_btree" - }, - "accessor_name": { - "some": "wallet_ledger_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "profile_wallet_ledger_wallet_ledger_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "puzzle_agent_message", - "product_type_ref": 247, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "puzzle_agent_message_message_id_idx_btree" - }, - "accessor_name": { - "some": "message_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "puzzle_agent_message_session_id_idx_btree" - }, - "accessor_name": { - "some": "by_puzzle_agent_message_session_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "puzzle_agent_message_message_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "puzzle_agent_session", - "product_type_ref": 250, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "puzzle_agent_session_owner_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_puzzle_agent_session_owner_user_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - }, - { - "name": { - "some": "puzzle_agent_session_session_id_idx_btree" - }, - "accessor_name": { - "some": "session_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "puzzle_agent_session_session_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "puzzle_runtime_run", - "product_type_ref": 251, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "puzzle_runtime_run_owner_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_puzzle_runtime_run_owner_user_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - }, - { - "name": { - "some": "puzzle_runtime_run_run_id_idx_btree" - }, - "accessor_name": { - "some": "run_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "puzzle_runtime_run_run_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "puzzle_work_profile", - "product_type_ref": 252, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "puzzle_work_profile_owner_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_puzzle_work_owner_user_id" - }, - "algorithm": { - "BTree": [ - 2 - ] - } - }, - { - "name": { - "some": "puzzle_work_profile_profile_id_idx_btree" - }, - "accessor_name": { - "some": "profile_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "puzzle_work_profile_publication_status_idx_btree" - }, - "accessor_name": { - "some": "by_puzzle_work_publication_status" - }, - "algorithm": { - "BTree": [ - 10 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "puzzle_work_profile_profile_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "quest_log", - "product_type_ref": 254, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "quest_log_actor_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_actor_user_id" - }, - "algorithm": { - "BTree": [ - 3 - ] - } - }, - { - "name": { - "some": "quest_log_log_id_idx_btree" - }, - "accessor_name": { - "some": "log_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "quest_log_quest_id_idx_btree" - }, - "accessor_name": { - "some": "by_quest_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - }, - { - "name": { - "some": "quest_log_runtime_session_id_idx_btree" - }, - "accessor_name": { - "some": "by_runtime_session_id" - }, - "algorithm": { - "BTree": [ - 2 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "quest_log_log_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "quest_record", - "product_type_ref": 257, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "quest_record_actor_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_actor_user_id" - }, - "algorithm": { - "BTree": [ - 3 - ] - } - }, - { - "name": { - "some": "quest_record_issuer_npc_id_idx_btree" - }, - "accessor_name": { - "some": "by_issuer_npc_id" - }, - "algorithm": { - "BTree": [ - 4 - ] - } - }, - { - "name": { - "some": "quest_record_quest_id_idx_btree" - }, - "accessor_name": { - "some": "quest_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "quest_record_runtime_session_id_idx_btree" - }, - "accessor_name": { - "some": "by_runtime_session_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "quest_record_quest_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "refresh_session", - "product_type_ref": 259, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "refresh_session_refresh_token_hash_idx_btree" - }, - "accessor_name": { - "some": "by_refresh_session_token_hash" - }, - "algorithm": { - "BTree": [ - 2 - ] - } - }, - { - "name": { - "some": "refresh_session_session_id_idx_btree" - }, - "accessor_name": { - "some": "session_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "refresh_session_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_refresh_session_user_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "refresh_session_session_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "runtime_setting", - "product_type_ref": 280, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "runtime_setting_user_id_idx_btree" - }, - "accessor_name": { - "some": "user_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "runtime_setting_user_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "runtime_snapshot", - "product_type_ref": 281, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "runtime_snapshot_user_id_idx_btree" - }, - "accessor_name": { - "some": "user_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "runtime_snapshot_user_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "story_event", - "product_type_ref": 288, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "story_event_event_id_idx_btree" - }, - "accessor_name": { - "some": "event_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "story_event_story_session_id_idx_btree" - }, - "accessor_name": { - "some": "by_story_session_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "story_event_event_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "story_session", - "product_type_ref": 289, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "story_session_actor_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_actor_user_id" - }, - "algorithm": { - "BTree": [ - 2 - ] - } - }, - { - "name": { - "some": "story_session_runtime_session_id_idx_btree" - }, - "accessor_name": { - "some": "by_runtime_session_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - }, - { - "name": { - "some": "story_session_story_session_id_idx_btree" - }, - "accessor_name": { - "some": "story_session_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "story_session_story_session_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "treasure_record", - "product_type_ref": 295, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "treasure_record_actor_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_treasure_actor_user_id" - }, - "algorithm": { - "BTree": [ - 3 - ] - } - }, - { - "name": { - "some": "treasure_record_encounter_id_idx_btree" - }, - "accessor_name": { - "some": "by_treasure_encounter_id" - }, - "algorithm": { - "BTree": [ - 4 - ] - } - }, - { - "name": { - "some": "treasure_record_runtime_session_id_idx_btree" - }, - "accessor_name": { - "some": "by_treasure_runtime_session_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - }, - { - "name": { - "some": "treasure_record_story_session_id_idx_btree" - }, - "accessor_name": { - "some": "by_treasure_story_session_id" - }, - "algorithm": { - "BTree": [ - 2 - ] - } - }, - { - "name": { - "some": "treasure_record_treasure_record_id_idx_btree" - }, - "accessor_name": { - "some": "treasure_record_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "treasure_record_treasure_record_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "user_account", - "product_type_ref": 308, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "user_account_public_user_code_idx_btree" - }, - "accessor_name": { - "some": "by_user_account_public_code" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - }, - { - "name": { - "some": "user_account_user_id_idx_btree" - }, - "accessor_name": { - "some": "user_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "user_account_username_idx_btree" - }, - "accessor_name": { - "some": "by_user_account_username" - }, - "algorithm": { - "BTree": [ - 2 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "user_account_user_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - }, - { - "name": "user_browse_history", - "product_type_ref": 309, - "primary_key": [ - 0 - ], - "indexes": [ - { - "name": { - "some": "user_browse_history_browse_history_id_idx_btree" - }, - "accessor_name": { - "some": "browse_history_id" - }, - "algorithm": { - "BTree": [ - 0 - ] - } - }, - { - "name": { - "some": "user_browse_history_user_id_idx_btree" - }, - "accessor_name": { - "some": "by_browse_history_user_id" - }, - "algorithm": { - "BTree": [ - 1 - ] - } - }, - { - "name": { - "some": "user_browse_history_user_id_owner_user_id_profile_id_idx_btree" - }, - "accessor_name": { - "some": "by_browse_history_user_owner_profile" - }, - "algorithm": { - "BTree": [ - 1, - 2, - 3 - ] - } - } - ], - "constraints": [ - { - "name": { - "some": "user_browse_history_browse_history_id_key" - }, - "data": { - "Unique": { - "columns": [ - 0 - ] - } - } - } - ], - "sequences": [], - "schedule": { - "none": [] - }, - "table_type": { - "User": [] - }, - "table_access": { - "Private": [] - } - } - ], - "reducers": [ - { - "name": "accept_quest", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 0 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "acknowledge_quest_completion", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 12 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "apply_chapter_progression_ledger_entry", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 30 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "apply_inventory_mutation", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 34 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "apply_quest_signal", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 44 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "begin_story_session", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 65 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "bind_asset_object_to_entity", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 81 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "confirm_asset_object", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 111 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "continue_story", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 114 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "create_ai_task", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 115 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "create_battle_state", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 117 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "grant_player_progression_experience", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 214 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "init", - "params": { - "elements": [] - }, - "lifecycle": { - "some": { - "Init": [] - } - } - }, - { - "name": "publish_custom_world_profile", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 243 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "resolve_combat_action", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 260 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "resolve_npc_interaction", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 264 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "resolve_npc_social_action", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 272 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "resolve_treasure_interaction", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 275 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "start_ai_task", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 284 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "start_ai_task_stage", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 285 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "turn_in_quest", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 296 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "unpublish_custom_world_profile", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 297 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "upsert_chapter_progression", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 300 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "upsert_custom_world_profile", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 302 - } - } - ] - }, - "lifecycle": { - "none": [] - } - }, - { - "name": "upsert_npc_state", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 303 - } - } - ] - }, - "lifecycle": { - "none": [] - } - } - ], - "types": [ - { - "name": { - "scope": [], - "name": "AiResultReference" - }, - "ty": 15, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiResultReferenceInput" - }, - "ty": 55, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiResultReferenceKind" - }, - "ty": 16, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiResultReferenceSnapshot" - }, - "ty": 28, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiStageCompletionInput" - }, - "ty": 109, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiTask" - }, - "ty": 17, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiTaskCancelInput" - }, - "ty": 84, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiTaskCreateInput" - }, - "ty": 115, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiTaskFailureInput" - }, - "ty": 161, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiTaskFinishInput" - }, - "ty": 110, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiTaskKind" - }, - "ty": 18, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiTaskProcedureResult" - }, - "ty": 25, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiTaskSnapshot" - }, - "ty": 26, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiTaskStage" - }, - "ty": 20, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiTaskStageBlueprint" - }, - "ty": 116, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiTaskStageKind" - }, - "ty": 21, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiTaskStageSnapshot" - }, - "ty": 27, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiTaskStageStartInput" - }, - "ty": 285, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiTaskStageStatus" - }, - "ty": 22, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiTaskStartInput" - }, - "ty": 284, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiTaskStatus" - }, - "ty": 19, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiTextChunk" - }, - "ty": 23, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiTextChunkAppendInput" - }, - "ty": 24, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AiTextChunkSnapshot" - }, - "ty": 29, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AssetEntityBinding" - }, - "ty": 52, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AssetEntityBindingInput" - }, - "ty": 81, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AssetEntityBindingProcedureResult" - }, - "ty": 82, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AssetEntityBindingSnapshot" - }, - "ty": 83, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AssetObject" - }, - "ty": 53, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AssetObjectAccessPolicy" - }, - "ty": 54, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AssetObjectProcedureResult" - }, - "ty": 112, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AssetObjectUpsertInput" - }, - "ty": 111, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AssetObjectUpsertSnapshot" - }, - "ty": 113, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AuthIdentity" - }, - "ty": 56, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AuthStoreSnapshot" - }, - "ty": 57, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AuthStoreSnapshotImportProcedureResult" - }, - "ty": 215, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AuthStoreSnapshotImportRecord" - }, - "ty": 216, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AuthStoreSnapshotProcedureResult" - }, - "ty": 159, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AuthStoreSnapshotRecord" - }, - "ty": 160, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "AuthStoreSnapshotUpsertInput" - }, - "ty": 299, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BattleMode" - }, - "ty": 59, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BattleState" - }, - "ty": 58, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BattleStateInput" - }, - "ty": 117, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BattleStateProcedureResult" - }, - "ty": 118, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BattleStateQueryInput" - }, - "ty": 168, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BattleStateSnapshot" - }, - "ty": 119, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BattleStatus" - }, - "ty": 60, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishAgentMessage" - }, - "ty": 71, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishAgentMessageKind" - }, - "ty": 73, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishAgentMessageRole" - }, - "ty": 72, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishAgentMessageSnapshot" - }, - "ty": 102, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishAnchorItem" - }, - "ty": 94, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishAnchorPack" - }, - "ty": 93, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishAnchorStatus" - }, - "ty": 95, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishAssetCoverage" - }, - "ty": 101, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishAssetGenerateInput" - }, - "ty": 167, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishAssetKind" - }, - "ty": 75, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishAssetSlot" - }, - "ty": 74, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishAssetSlotSnapshot" - }, - "ty": 100, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishAssetStatus" - }, - "ty": 76, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishBackgroundBlueprint" - }, - "ty": 98, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishCreationSession" - }, - "ty": 77, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishCreationStage" - }, - "ty": 78, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishDraftCompileInput" - }, - "ty": 90, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishGameDraft" - }, - "ty": 96, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishLevelBlueprint" - }, - "ty": 97, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishMessageFinalizeInput" - }, - "ty": 162, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishMessageSubmitInput" - }, - "ty": 291, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishPublishInput" - }, - "ty": 242, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishRunGetInput" - }, - "ty": 169, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishRunInputSubmitInput" - }, - "ty": 290, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishRunProcedureResult" - }, - "ty": 170, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishRunStartInput" - }, - "ty": 286, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishRunStatus" - }, - "ty": 80, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishRuntimeEntity" - }, - "ty": 172, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishRuntimeParams" - }, - "ty": 99, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishRuntimeRun" - }, - "ty": 79, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishRuntimeSnapshot" - }, - "ty": 171, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishSessionCreateInput" - }, - "ty": 120, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishSessionGetInput" - }, - "ty": 174, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishSessionProcedureResult" - }, - "ty": 91, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishSessionSnapshot" - }, - "ty": 92, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishVector2" - }, - "ty": 173, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishWorkDeleteInput" - }, - "ty": 146, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishWorksListInput" - }, - "ty": 218, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "BigFishWorksProcedureResult" - }, - "ty": 147, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "ChapterPaceBand" - }, - "ty": 33, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "ChapterProgression" - }, - "ty": 85, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "ChapterProgressionGetInput" - }, - "ty": 175, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "ChapterProgressionInput" - }, - "ty": 300, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "ChapterProgressionLedgerInput" - }, - "ty": 30, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "ChapterProgressionProcedureResult" - }, - "ty": 31, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "ChapterProgressionSnapshot" - }, - "ty": 32, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CombatOutcome" - }, - "ty": 64, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "ConsumeInventoryItemInput" - }, - "ty": 41, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldAgentActionExecuteInput" - }, - "ty": 157, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldAgentActionExecuteResult" - }, - "ty": 158, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldAgentCardDetailGetInput" - }, - "ty": 176, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldAgentMessage" - }, - "ty": 136, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldAgentMessageFinalizeInput" - }, - "ty": 163, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldAgentMessageSnapshot" - }, - "ty": 125, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldAgentMessageSubmitInput" - }, - "ty": 292, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldAgentOperation" - }, - "ty": 137, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldAgentOperationGetInput" - }, - "ty": 180, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldAgentOperationProcedureResult" - }, - "ty": 164, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldAgentOperationProgressInput" - }, - "ty": 301, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldAgentOperationSnapshot" - }, - "ty": 132, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldAgentSession" - }, - "ty": 138, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldAgentSessionCreateInput" - }, - "ty": 121, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldAgentSessionGetInput" - }, - "ty": 181, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldAgentSessionProcedureResult" - }, - "ty": 122, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldAgentSessionSnapshot" - }, - "ty": 123, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldDraftCard" - }, - "ty": 139, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldDraftCardDetailResult" - }, - "ty": 177, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldDraftCardDetailSectionSnapshot" - }, - "ty": 179, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldDraftCardDetailSnapshot" - }, - "ty": 178, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldDraftCardSnapshot" - }, - "ty": 128, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldGalleryDetailByCodeInput" - }, - "ty": 185, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldGalleryDetailInput" - }, - "ty": 182, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldGalleryEntry" - }, - "ty": 140, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldGalleryEntrySnapshot" - }, - "ty": 184, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldGalleryListResult" - }, - "ty": 219, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldGenerationMode" - }, - "ty": 144, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldLibraryDetailInput" - }, - "ty": 186, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldLibraryMutationResult" - }, - "ty": 183, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldProfile" - }, - "ty": 141, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldProfileDeleteInput" - }, - "ty": 148, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldProfileListInput" - }, - "ty": 220, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldProfileListResult" - }, - "ty": 149, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldProfilePublishInput" - }, - "ty": 243, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldProfileSnapshot" - }, - "ty": 150, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldProfileUnpublishInput" - }, - "ty": 297, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldProfileUpsertInput" - }, - "ty": 302, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldPublicationStatus" - }, - "ty": 142, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldPublishWorldInput" - }, - "ty": 244, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldPublishWorldResult" - }, - "ty": 245, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldPublishedProfileCompileInput" - }, - "ty": 103, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldPublishedProfileCompileResult" - }, - "ty": 104, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldPublishedProfileCompileSnapshot" - }, - "ty": 105, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldRoleAssetStatus" - }, - "ty": 131, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldSession" - }, - "ty": 143, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldSessionStatus" - }, - "ty": 145, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldThemeMode" - }, - "ty": 106, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldWorkSummarySnapshot" - }, - "ty": 223, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldWorksListInput" - }, - "ty": 221, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "CustomWorldWorksListResult" - }, - "ty": 222, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "EquipInventoryItemInput" - }, - "ty": 42, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "GrantInventoryItemInput" - }, - "ty": 36, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "InventoryContainerKind" - }, - "ty": 206, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "InventoryEquipmentSlot" - }, - "ty": 39, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "InventoryItemRarity" - }, - "ty": 38, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "InventoryItemSnapshot" - }, - "ty": 37, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "InventoryItemSourceKind" - }, - "ty": 40, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "InventoryMutation" - }, - "ty": 35, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "InventoryMutationInput" - }, - "ty": 34, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "InventorySlot" - }, - "ty": 217, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "InventorySlotSnapshot" - }, - "ty": 205, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "NpcBattleInteractionProcedureResult" - }, - "ty": 265, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "NpcBattleInteractionResult" - }, - "ty": 266, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "NpcInteractionBattleMode" - }, - "ty": 270, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "NpcInteractionProcedureResult" - }, - "ty": 271, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "NpcInteractionResult" - }, - "ty": 267, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "NpcInteractionStatus" - }, - "ty": 269, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "NpcRelationStance" - }, - "ty": 235, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "NpcRelationState" - }, - "ty": 234, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "NpcSocialActionKind" - }, - "ty": 273, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "NpcStanceProfile" - }, - "ty": 236, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "NpcState" - }, - "ty": 233, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "NpcStateProcedureResult" - }, - "ty": 274, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "NpcStateSnapshot" - }, - "ty": 268, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "NpcStateUpsertInput" - }, - "ty": 303, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PlayerProgression" - }, - "ty": 237, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PlayerProgressionGetInput" - }, - "ty": 187, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PlayerProgressionGrantInput" - }, - "ty": 214, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PlayerProgressionGrantSource" - }, - "ty": 190, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PlayerProgressionProcedureResult" - }, - "ty": 188, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PlayerProgressionSnapshot" - }, - "ty": 189, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "ProfileDashboardState" - }, - "ty": 238, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "ProfilePlayedWorld" - }, - "ty": 239, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "ProfileSaveArchive" - }, - "ty": 240, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "ProfileWalletLedger" - }, - "ty": 241, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleAgentMessageFinalizeInput" - }, - "ty": 165, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleAgentMessageKind" - }, - "ty": 249, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleAgentMessageRole" - }, - "ty": 248, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleAgentMessageRow" - }, - "ty": 247, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleAgentMessageSubmitInput" - }, - "ty": 293, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleAgentSessionCreateInput" - }, - "ty": 135, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleAgentSessionGetInput" - }, - "ty": 198, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleAgentSessionProcedureResult" - }, - "ty": 108, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleAgentSessionRow" - }, - "ty": 250, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleAgentStage" - }, - "ty": 166, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleDraftCompileInput" - }, - "ty": 107, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleGeneratedImagesSaveInput" - }, - "ty": 282, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzlePublicationStatus" - }, - "ty": 253, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzlePublishInput" - }, - "ty": 246, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleRunDragInput" - }, - "ty": 156, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleRunGetInput" - }, - "ty": 201, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleRunNextLevelInput" - }, - "ty": 13, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleRunProcedureResult" - }, - "ty": 14, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleRunStartInput" - }, - "ty": 287, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleRunSwapInput" - }, - "ty": 294, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleRuntimeRunRow" - }, - "ty": 251, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleSelectCoverImageInput" - }, - "ty": 283, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleWorkDeleteInput" - }, - "ty": 151, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleWorkGetInput" - }, - "ty": 199, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleWorkProcedureResult" - }, - "ty": 200, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleWorkProfileRow" - }, - "ty": 252, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleWorkUpsertInput" - }, - "ty": 298, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleWorksListInput" - }, - "ty": 232, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "PuzzleWorksProcedureResult" - }, - "ty": 152, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestCompletionAckInput" - }, - "ty": 12, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestHostileNpcDefeatedSignal" - }, - "ty": 46, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestItemDeliveredSignal" - }, - "ty": 51, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestLog" - }, - "ty": 254, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestLogEventKind" - }, - "ty": 255, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestNarrativeBindingSnapshot" - }, - "ty": 7, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestNarrativeOrigin" - }, - "ty": 8, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestNarrativeType" - }, - "ty": 9, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestNpcSparCompletedSignal" - }, - "ty": 48, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestNpcTalkCompletedSignal" - }, - "ty": 49, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestObjectiveKind" - }, - "ty": 11, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestObjectiveSnapshot" - }, - "ty": 258, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestProgressSignal" - }, - "ty": 45, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestRecord" - }, - "ty": 257, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestRecordInput" - }, - "ty": 0, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestRewardEquipmentSlot" - }, - "ty": 5, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestRewardIntel" - }, - "ty": 6, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestRewardItem" - }, - "ty": 3, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestRewardItemRarity" - }, - "ty": 4, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestRewardSnapshot" - }, - "ty": 2, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestSceneReachedSignal" - }, - "ty": 50, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestSignalApplyInput" - }, - "ty": 44, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestSignalKind" - }, - "ty": 256, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestStatus" - }, - "ty": 1, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestStepSnapshot" - }, - "ty": 10, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestTreasureInspectedSignal" - }, - "ty": 47, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "QuestTurnInInput" - }, - "ty": 296, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RefreshSession" - }, - "ty": 259, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "ResolveCombatActionInput" - }, - "ty": 260, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "ResolveCombatActionProcedureResult" - }, - "ty": 261, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "ResolveCombatActionResult" - }, - "ty": 262, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "ResolveNpcBattleInteractionInput" - }, - "ty": 263, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "ResolveNpcInteractionInput" - }, - "ty": 264, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "ResolveNpcSocialActionInput" - }, - "ty": 272, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RpgAgentDraftCardKind" - }, - "ty": 129, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RpgAgentDraftCardStatus" - }, - "ty": 130, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RpgAgentMessageKind" - }, - "ty": 127, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RpgAgentMessageRole" - }, - "ty": 126, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RpgAgentOperationStatus" - }, - "ty": 134, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RpgAgentOperationType" - }, - "ty": 133, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RpgAgentStage" - }, - "ty": 124, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeBrowseHistoryClearInput" - }, - "ty": 86, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeBrowseHistoryListInput" - }, - "ty": 224, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeBrowseHistoryProcedureResult" - }, - "ty": 87, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeBrowseHistorySnapshot" - }, - "ty": 88, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeBrowseHistorySyncInput" - }, - "ty": 304, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeBrowseHistoryThemeMode" - }, - "ty": 89, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeBrowseHistoryWriteInput" - }, - "ty": 305, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeInventoryStateProcedureResult" - }, - "ty": 203, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeInventoryStateQueryInput" - }, - "ty": 202, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeInventoryStateSnapshot" - }, - "ty": 204, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeItemEquipmentSlot" - }, - "ty": 63, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeItemRewardItemRarity" - }, - "ty": 62, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeItemRewardItemSnapshot" - }, - "ty": 61, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimePlatformTheme" - }, - "ty": 210, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeProfileDashboardGetInput" - }, - "ty": 191, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeProfileDashboardProcedureResult" - }, - "ty": 192, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeProfileDashboardSnapshot" - }, - "ty": 193, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeProfilePlayStatsGetInput" - }, - "ty": 194, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeProfilePlayStatsProcedureResult" - }, - "ty": 195, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeProfilePlayStatsSnapshot" - }, - "ty": 196, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeProfilePlayedWorldSnapshot" - }, - "ty": 197, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeProfileSaveArchiveListInput" - }, - "ty": 225, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeProfileSaveArchiveProcedureResult" - }, - "ty": 226, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeProfileSaveArchiveResumeInput" - }, - "ty": 279, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeProfileSaveArchiveSnapshot" - }, - "ty": 227, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeProfileWalletLedgerEntrySnapshot" - }, - "ty": 230, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeProfileWalletLedgerListInput" - }, - "ty": 228, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeProfileWalletLedgerProcedureResult" - }, - "ty": 229, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeProfileWalletLedgerSourceType" - }, - "ty": 231, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeSetting" - }, - "ty": 280, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeSettingGetInput" - }, - "ty": 207, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeSettingProcedureResult" - }, - "ty": 208, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeSettingSnapshot" - }, - "ty": 209, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeSettingUpsertInput" - }, - "ty": 306, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeSnapshot" - }, - "ty": 155, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeSnapshotDeleteInput" - }, - "ty": 153, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeSnapshotGetInput" - }, - "ty": 211, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeSnapshotProcedureResult" - }, - "ty": 154, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeSnapshotRow" - }, - "ty": 281, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "RuntimeSnapshotUpsertInput" - }, - "ty": 307, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "StoryContinueInput" - }, - "ty": 114, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "StoryEvent" - }, - "ty": 288, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "StoryEventKind" - }, - "ty": 70, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "StoryEventSnapshot" - }, - "ty": 69, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "StorySession" - }, - "ty": 289, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "StorySessionInput" - }, - "ty": 65, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "StorySessionProcedureResult" - }, - "ty": 66, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "StorySessionSnapshot" - }, - "ty": 67, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "StorySessionStateInput" - }, - "ty": 212, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "StorySessionStateProcedureResult" - }, - "ty": 213, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "StorySessionStatus" - }, - "ty": 68, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "TreasureInteractionAction" - }, - "ty": 276, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "TreasureRecord" - }, - "ty": 295, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "TreasureRecordProcedureResult" - }, - "ty": 277, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "TreasureRecordSnapshot" - }, - "ty": 278, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "TreasureResolveInput" - }, - "ty": 275, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "UnequipInventoryItemInput" - }, - "ty": 43, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "UserAccount" - }, - "ty": 308, - "custom_ordering": true - }, - { - "name": { - "scope": [], - "name": "UserBrowseHistory" - }, - "ty": 309, - "custom_ordering": true - } - ], - "misc_exports": [ - { - "Procedure": { - "name": "advance_puzzle_next_level", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 13 - } - } - ] - }, - "return_type": { - "Ref": 14 - } - } - }, - { - "Procedure": { - "name": "append_ai_text_chunk_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 24 - } - } - ] - }, - "return_type": { - "Ref": 25 - } - } - }, - { - "Procedure": { - "name": "apply_chapter_progression_ledger_entry_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 30 - } - } - ] - }, - "return_type": { - "Ref": 31 - } - } - }, - { - "Procedure": { - "name": "attach_ai_result_reference_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 55 - } - } - ] - }, - "return_type": { - "Ref": 25 - } - } - }, - { - "Procedure": { - "name": "begin_story_session_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 65 - } - } - ] - }, - "return_type": { - "Ref": 66 - } - } - }, - { - "Procedure": { - "name": "bind_asset_object_to_entity_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 81 - } - } - ] - }, - "return_type": { - "Ref": 82 - } - } - }, - { - "Procedure": { - "name": "cancel_ai_task_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 84 - } - } - ] - }, - "return_type": { - "Ref": 25 - } - } - }, - { - "Procedure": { - "name": "clear_platform_browse_history_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 86 - } - } - ] - }, - "return_type": { - "Ref": 87 - } - } - }, - { - "Procedure": { - "name": "compile_big_fish_draft", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 90 - } - } - ] - }, - "return_type": { - "Ref": 91 - } - } - }, - { - "Procedure": { - "name": "compile_custom_world_published_profile", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 103 - } - } - ] - }, - "return_type": { - "Ref": 104 - } - } - }, - { - "Procedure": { - "name": "compile_puzzle_agent_draft", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 107 - } - } - ] - }, - "return_type": { - "Ref": 108 - } - } - }, - { - "Procedure": { - "name": "complete_ai_stage_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 109 - } - } - ] - }, - "return_type": { - "Ref": 25 - } - } - }, - { - "Procedure": { - "name": "complete_ai_task_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 110 - } - } - ] - }, - "return_type": { - "Ref": 25 - } - } - }, - { - "Procedure": { - "name": "confirm_asset_object_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 111 - } - } - ] - }, - "return_type": { - "Ref": 112 - } - } - }, - { - "Procedure": { - "name": "continue_story_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 114 - } - } - ] - }, - "return_type": { - "Ref": 66 - } - } - }, - { - "Procedure": { - "name": "create_ai_task_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 115 - } - } - ] - }, - "return_type": { - "Ref": 25 - } - } - }, - { - "Procedure": { - "name": "create_battle_state_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 117 - } - } - ] - }, - "return_type": { - "Ref": 118 - } - } - }, - { - "Procedure": { - "name": "create_big_fish_session", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 120 - } - } - ] - }, - "return_type": { - "Ref": 91 - } - } - }, - { - "Procedure": { - "name": "create_custom_world_agent_session", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 121 - } - } - ] - }, - "return_type": { - "Ref": 122 - } - } - }, - { - "Procedure": { - "name": "create_puzzle_agent_session", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 135 - } - } - ] - }, - "return_type": { - "Ref": 108 - } - } - }, - { - "Procedure": { - "name": "delete_big_fish_work", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 146 - } - } - ] - }, - "return_type": { - "Ref": 147 - } - } - }, - { - "Procedure": { - "name": "delete_custom_world_profile_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 148 - } - } - ] - }, - "return_type": { - "Ref": 149 - } - } - }, - { - "Procedure": { - "name": "delete_puzzle_work", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 151 - } - } - ] - }, - "return_type": { - "Ref": 152 - } - } - }, - { - "Procedure": { - "name": "delete_runtime_snapshot_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 153 - } - } - ] - }, - "return_type": { - "Ref": 154 - } - } - }, - { - "Procedure": { - "name": "drag_puzzle_piece_or_group", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 156 - } - } - ] - }, - "return_type": { - "Ref": 14 - } - } - }, - { - "Procedure": { - "name": "execute_custom_world_agent_action", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 157 - } - } - ] - }, - "return_type": { - "Ref": 158 - } - } - }, - { - "Procedure": { - "name": "export_auth_store_snapshot_from_tables", - "params": { - "elements": [] - }, - "return_type": { - "Ref": 159 - } - } - }, - { - "Procedure": { - "name": "fail_ai_task_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 161 - } - } - ] - }, - "return_type": { - "Ref": 25 - } - } - }, - { - "Procedure": { - "name": "finalize_big_fish_agent_message_turn", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 162 - } - } - ] - }, - "return_type": { - "Ref": 91 - } - } - }, - { - "Procedure": { - "name": "finalize_custom_world_agent_message_turn", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 163 - } - } - ] - }, - "return_type": { - "Ref": 164 - } - } - }, - { - "Procedure": { - "name": "finalize_puzzle_agent_message_turn", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 165 - } - } - ] - }, - "return_type": { - "Ref": 108 - } - } - }, - { - "Procedure": { - "name": "generate_big_fish_asset", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 167 - } - } - ] - }, - "return_type": { - "Ref": 91 - } - } - }, - { - "Procedure": { - "name": "get_auth_store_snapshot", - "params": { - "elements": [] - }, - "return_type": { - "Ref": 159 - } - } - }, - { - "Procedure": { - "name": "get_battle_state", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 168 - } - } - ] - }, - "return_type": { - "Ref": 118 - } - } - }, - { - "Procedure": { - "name": "get_big_fish_run", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 169 - } - } - ] - }, - "return_type": { - "Ref": 170 - } - } - }, - { - "Procedure": { - "name": "get_big_fish_session", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 174 - } - } - ] - }, - "return_type": { - "Ref": 91 - } - } - }, - { - "Procedure": { - "name": "get_chapter_progression", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 175 - } - } - ] - }, - "return_type": { - "Ref": 31 - } - } - }, - { - "Procedure": { - "name": "get_custom_world_agent_card_detail", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 176 - } - } - ] - }, - "return_type": { - "Ref": 177 - } - } - }, - { - "Procedure": { - "name": "get_custom_world_agent_operation", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 180 - } - } - ] - }, - "return_type": { - "Ref": 164 - } - } - }, - { - "Procedure": { - "name": "get_custom_world_agent_session", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 181 - } - } - ] - }, - "return_type": { - "Ref": 122 - } - } - }, - { - "Procedure": { - "name": "get_custom_world_gallery_detail", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 182 - } - } - ] - }, - "return_type": { - "Ref": 183 - } - } - }, - { - "Procedure": { - "name": "get_custom_world_gallery_detail_by_code", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 185 - } - } - ] - }, - "return_type": { - "Ref": 183 - } - } - }, - { - "Procedure": { - "name": "get_custom_world_library_detail", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 186 - } - } - ] - }, - "return_type": { - "Ref": 183 - } - } - }, - { - "Procedure": { - "name": "get_player_progression_or_default", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 187 - } - } - ] - }, - "return_type": { - "Ref": 188 - } - } - }, - { - "Procedure": { - "name": "get_profile_dashboard", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 191 - } - } - ] - }, - "return_type": { - "Ref": 192 - } - } - }, - { - "Procedure": { - "name": "get_profile_play_stats", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 194 - } - } - ] - }, - "return_type": { - "Ref": 195 - } - } - }, - { - "Procedure": { - "name": "get_puzzle_agent_session", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 198 - } - } - ] - }, - "return_type": { - "Ref": 108 - } - } - }, - { - "Procedure": { - "name": "get_puzzle_gallery_detail", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 199 - } - } - ] - }, - "return_type": { - "Ref": 200 - } - } - }, - { - "Procedure": { - "name": "get_puzzle_run", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 201 - } - } - ] - }, - "return_type": { - "Ref": 14 - } - } - }, - { - "Procedure": { - "name": "get_puzzle_work_detail", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 199 - } - } - ] - }, - "return_type": { - "Ref": 200 - } - } - }, - { - "Procedure": { - "name": "get_runtime_inventory_state", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 202 - } - } - ] - }, - "return_type": { - "Ref": 203 - } - } - }, - { - "Procedure": { - "name": "get_runtime_setting_or_default", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 207 - } - } - ] - }, - "return_type": { - "Ref": 208 - } - } - }, - { - "Procedure": { - "name": "get_runtime_snapshot", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 211 - } - } - ] - }, - "return_type": { - "Ref": 154 - } - } - }, - { - "Procedure": { - "name": "get_story_session_state", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 212 - } - } - ] - }, - "return_type": { - "Ref": 213 - } - } - }, - { - "Procedure": { - "name": "grant_player_progression_experience_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 214 - } - } - ] - }, - "return_type": { - "Ref": 188 - } - } - }, - { - "Procedure": { - "name": "import_auth_store_snapshot", - "params": { - "elements": [] - }, - "return_type": { - "Ref": 215 - } - } - }, - { - "Procedure": { - "name": "list_big_fish_works", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 218 - } - } - ] - }, - "return_type": { - "Ref": 147 - } - } - }, - { - "Procedure": { - "name": "list_custom_world_gallery_entries", - "params": { - "elements": [] - }, - "return_type": { - "Ref": 219 - } - } - }, - { - "Procedure": { - "name": "list_custom_world_profiles", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 220 - } - } - ] - }, - "return_type": { - "Ref": 149 - } - } - }, - { - "Procedure": { - "name": "list_custom_world_works", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 221 - } - } - ] - }, - "return_type": { - "Ref": 222 - } - } - }, - { - "Procedure": { - "name": "list_platform_browse_history", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 224 - } - } - ] - }, - "return_type": { - "Ref": 87 - } - } - }, - { - "Procedure": { - "name": "list_profile_save_archives", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 225 - } - } - ] - }, - "return_type": { - "Ref": 226 - } - } - }, - { - "Procedure": { - "name": "list_profile_wallet_ledger", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 228 - } - } - ] - }, - "return_type": { - "Ref": 229 - } - } - }, - { - "Procedure": { - "name": "list_puzzle_gallery", - "params": { - "elements": [] - }, - "return_type": { - "Ref": 152 - } - } - }, - { - "Procedure": { - "name": "list_puzzle_works", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 232 - } - } - ] - }, - "return_type": { - "Ref": 152 - } - } - }, - { - "Procedure": { - "name": "publish_big_fish_game", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 242 - } - } - ] - }, - "return_type": { - "Ref": 91 - } - } - }, - { - "Procedure": { - "name": "publish_custom_world_profile_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 243 - } - } - ] - }, - "return_type": { - "Ref": 183 - } - } - }, - { - "Procedure": { - "name": "publish_custom_world_world", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 244 - } - } - ] - }, - "return_type": { - "Ref": 245 - } - } - }, - { - "Procedure": { - "name": "publish_puzzle_work", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 246 - } - } - ] - }, - "return_type": { - "Ref": 200 - } - } - }, - { - "Procedure": { - "name": "resolve_combat_action_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 260 - } - } - ] - }, - "return_type": { - "Ref": 261 - } - } - }, - { - "Procedure": { - "name": "resolve_npc_battle_interaction_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 263 - } - } - ] - }, - "return_type": { - "Ref": 265 - } - } - }, - { - "Procedure": { - "name": "resolve_npc_interaction_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 264 - } - } - ] - }, - "return_type": { - "Ref": 271 - } - } - }, - { - "Procedure": { - "name": "resolve_npc_social_action_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 272 - } - } - ] - }, - "return_type": { - "Ref": 274 - } - } - }, - { - "Procedure": { - "name": "resolve_treasure_interaction_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 275 - } - } - ] - }, - "return_type": { - "Ref": 277 - } - } - }, - { - "Procedure": { - "name": "resume_profile_save_archive_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 279 - } - } - ] - }, - "return_type": { - "Ref": 226 - } - } - }, - { - "Procedure": { - "name": "save_puzzle_generated_images", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 282 - } - } - ] - }, - "return_type": { - "Ref": 108 - } - } - }, - { - "Procedure": { - "name": "select_puzzle_cover_image", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 283 - } - } - ] - }, - "return_type": { - "Ref": 108 - } - } - }, - { - "Procedure": { - "name": "start_big_fish_run", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 286 - } - } - ] - }, - "return_type": { - "Ref": 170 - } - } - }, - { - "Procedure": { - "name": "start_puzzle_run", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 287 - } - } - ] - }, - "return_type": { - "Ref": 14 - } - } - }, - { - "Procedure": { - "name": "submit_big_fish_input", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 290 - } - } - ] - }, - "return_type": { - "Ref": 170 - } - } - }, - { - "Procedure": { - "name": "submit_big_fish_message", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 291 - } - } - ] - }, - "return_type": { - "Ref": 91 - } - } - }, - { - "Procedure": { - "name": "submit_custom_world_agent_message", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 292 - } - } - ] - }, - "return_type": { - "Ref": 164 - } - } - }, - { - "Procedure": { - "name": "submit_puzzle_agent_message", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 293 - } - } - ] - }, - "return_type": { - "Ref": 108 - } - } - }, - { - "Procedure": { - "name": "swap_puzzle_pieces", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 294 - } - } - ] - }, - "return_type": { - "Ref": 14 - } - } - }, - { - "Procedure": { - "name": "unpublish_custom_world_profile_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 297 - } - } - ] - }, - "return_type": { - "Ref": 183 - } - } - }, - { - "Procedure": { - "name": "update_puzzle_work", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 298 - } - } - ] - }, - "return_type": { - "Ref": 200 - } - } - }, - { - "Procedure": { - "name": "upsert_auth_store_snapshot", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 299 - } - } - ] - }, - "return_type": { - "Ref": 159 - } - } - }, - { - "Procedure": { - "name": "upsert_chapter_progression_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 300 - } - } - ] - }, - "return_type": { - "Ref": 31 - } - } - }, - { - "Procedure": { - "name": "upsert_custom_world_agent_operation_progress", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 301 - } - } - ] - }, - "return_type": { - "Ref": 164 - } - } - }, - { - "Procedure": { - "name": "upsert_custom_world_profile_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 302 - } - } - ] - }, - "return_type": { - "Ref": 183 - } - } - }, - { - "Procedure": { - "name": "upsert_npc_state_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 303 - } - } - ] - }, - "return_type": { - "Ref": 274 - } - } - }, - { - "Procedure": { - "name": "upsert_platform_browse_history_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 304 - } - } - ] - }, - "return_type": { - "Ref": 87 - } - } - }, - { - "Procedure": { - "name": "upsert_runtime_setting_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 306 - } - } - ] - }, - "return_type": { - "Ref": 208 - } - } - }, - { - "Procedure": { - "name": "upsert_runtime_snapshot_and_return", - "params": { - "elements": [ - { - "name": { - "some": "input" - }, - "algebraic_type": { - "Ref": 307 - } - } - ] - }, - "return_type": { - "Ref": 154 - } - } - } - ], - "row_level_security": [] -} diff --git a/.gitignore b/.gitignore index 11a83c89..c90efe5c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,16 @@ coverage/ .DS_Store *.log /.codex-logs/ +/.codex/logs/ +/.codex/run-logs/ +/.codex/tmp-schema.json +/.codex-home-*.png /.codex-cargo-home-*/ /.codex-cache*/ /.tmp*/ /.idea/ +/bash.exe.stackdump +/test-code.ps1 .preview.* tmp_* tmp/ @@ -30,6 +36,7 @@ temp*build*/ /.codex-temp /target/ /logs +/server-rs/crates/*/logs/ .worktrees/ .env.secrets.local spacetime.local.json diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 769b1021..50476ac8 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -54,8 +54,9 @@ - 背景:`server-rs/crates/api-server/src/puzzle.rs` 已膨胀为数千行大文件,混合 Axum handler、草稿编译、图片生成、VectorEngine / OSS 持久化、DTO mapper、标签生成和测试;继续在单文件内迭代会降低定位和评审效率。 - 决策:原超大 `puzzle.rs` 改为同名入口 `server-rs/crates/api-server/src/puzzle.rs` 加 `server-rs/crates/api-server/src/puzzle/` 子模块目录。`puzzle.rs` 只保留聚合入口和 handler re-export;`handlers.rs` 放 HTTP handler;`draft.rs` 放表单草稿 / 编译 / snapshot helper;`generation.rs` 放图片与 UI 背景生成编排;`vector_engine.rs` 放 VectorEngine、下载、OSS、asset object / binding 和错误归一;`mappers.rs` / `tags.rs` 保留映射和标签 / 错误 helper;`tests.rs` 承接原 puzzle 单测。 +- 2026-05-21 追加决策:拼图 HTTP/BFF handler 不再直接提取完整 `AppState`,统一通过 `PuzzleApiState` 暴露拼图能力需要的 SpacetimeDB facade、gallery cache、OSS、作者查询、LLM 和少量配置快照。`modules/puzzle.rs` 仍接收全局 `AppState` 以挂接鉴权和回到全局路由树,但内部路由先 `.with_state(PuzzleApiState::from_ref(&state))`,handler 使用 `State`。确需复用计费、外部失败审计等仍要求 `AppState` 的横切 helper 时,先经 `PuzzleApiState::root_state()` 显式过渡,后续再继续收窄。 - 边界:本次只改变 `api-server` 内部文件组织,不改变 `/api/runtime/puzzle/*` 路由、DTO、error envelope、SpacetimeDB schema、公开 gallery cache 语义或计费语义。领域规则后续仍应逐步沉到 `module-puzzle`,SpacetimeDB 表、reducer、procedure 和 row shape 仍留在 `spacetime-module`。 -- 影响范围:`server-rs/crates/api-server/src/puzzle/`、`server-rs/crates/api-server/src/modules/puzzle.rs` 的 handler 引用、后端架构文档。 +- 影响范围:`server-rs/crates/api-server/src/state.rs`、`server-rs/crates/api-server/src/puzzle/`、`server-rs/crates/api-server/src/modules/puzzle.rs` 的 handler 引用、后端架构文档。 - 验证方式:执行 `cargo check -p api-server --manifest-path server-rs\Cargo.toml`;后续若改动 puzzle API 行为,再按对应路由补充定向测试和 `npm run dev:api-server` `/healthz` smoke。 - 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。 @@ -103,20 +104,14 @@ - 验证方式:执行 `npm run container:config` 展开 compose 配置;需要真实运行时再执行 `npm run container:build`、`npm run container:up`、`npm run container:k6`,并结合容器 Nginx log 与 OTLP debug exporter 判断瓶颈。 - 关联文档:`deploy/container/README.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 -## 2026-05-18 生产 provision 改为构建机准备工具包再上传安装 +## 2026-05-19 生产 provision 改为 Windows 下载包后由目标机本地安装 -- 背景:目标 release 服务器无法访问 GitHub,之前的 server provision 默认仍假设 `spacetime` 和 `otelcol-contrib` 已经存在于目标机本地路径,和真实运维条件不符。 -- 决策:Jenkins 新增 `Prepare Provision Tools` 阶段,在 `linux && genarrative-build` 构建机执行 `scripts/prepare-server-provision-tools.sh`,通过官方 SpacetimeDB 安装入口和 OpenTelemetry release 包生成 `provision-tools/`,再用 `stash/unstash` 带到 release 部署 agent;`scripts/jenkins-server-provision.sh` 只从工作区工具包复制安装,不再要求目标机自己下载或预装二进制。 +- 背景:当前 `development` provision 目标实际就是 Linux agent `genarrative-build-01`,之前把 `Prepare Provision Tools` 放在 `linux && genarrative-build` 会让目标机自己连 GitHub 和 `install.spacetimedb.com`,违背“Windows 本机先下载再传到目标机”的运维要求。 +- 决策:`Genarrative-Server-Provision` 拆成 Windows 下载阶段和 Linux 目标机安装阶段。Windows 节点的 `Download Provision Tool Archives` 只下载 `spacetime-x86_64-unknown-linux-gnu.tar.gz` 和 `otelcol-contrib_0.151.0_linux_amd64.tar.gz`,通过 `stash/unstash` 传到目标 Linux 节点;目标机执行 `scripts/prepare-server-provision-tools.sh` 时设置 `PROVISION_REQUIRE_LOCAL_DOWNLOADS=true`,只消费已下载件生成 `provision-tools/`,缺包直接失败,不回退外网下载。 +- 追加决策:Server-Provision 的 Windows helper 不再对 Jenkins `writeFile` 刚写出的 `.ps1` 做原地 UTF-8 BOM 重写,而是由显式 `powershell.exe` 按 UTF-8 读入脚本文本,并用 `ScriptBlock::Create(...)` 在内存中执行;这样既保留中文脚本内容,又避免同一个 workspace 脚本被立即重写时触发 `拒绝访问`。 +- 追加决策:GitHub release asset 的可用校验信息使用 `digest` 字段,实际是 `sha256:...`,不是 MD5;Windows 下载阶段先查 digest,再决定是否复用已有文件。 - 影响范围:`jenkins/Jenkinsfile.production-server-provision`、`scripts/prepare-server-provision-tools.sh`、`scripts/jenkins-server-provision.sh`、生产运维文档。 -- 验证方式:Jenkins 构建机可完成工具包准备,release 部署 agent 只消费工作区文件;目标机不再依赖 GitHub 外网下载。 -- 关联文档:`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 - -## 2026-05-19 otelcol-contrib 改为 Jenkins 手动上传归档再解包 - -- 背景:Genarrative-Server-Provision 中 `otelcol-contrib` 的构建机下载步骤耗时较长,且本机已经提前准备好安装包。 -- 决策:`jenkins/Jenkinsfile.production-server-provision` 新增 `OTELCOL_CONTRIB_ARCHIVE` 手动上传参数,默认要求上传 `otelcol-contrib_0.151.0_linux_amd64.tar.gz`;`scripts/prepare-server-provision-tools.sh` 优先从上传归档解包生成 `provision-tools/otelcol-contrib`,不再默认联网下载 OpenTelemetry release 包。 -- 影响范围:`jenkins/Jenkinsfile.production-server-provision`、`scripts/prepare-server-provision-tools.sh`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 -- 验证方式:Jenkins 日志应显示使用手动上传的 otelcol 包,`MANIFEST.txt` 记录 source 为 manual archive;当 `ENABLE_OTELCOL=false` 时可以跳过 collector 工具包准备。 +- 验证方式:Jenkins 日志应先出现 Windows 节点的 `[jenkins-powershell] workspace:`、`[jenkins-powershell] loaded bytes:` 和 `[prepare-provision-downloads]` 下载日志,再在 `genarrative-build-01` 上出现“使用已下载的 ...”日志;目标机不应出现直接访问 `install.spacetimedb.com` 或 OpenTelemetry GitHub release 下载地址的回退日志,且不再需要 `spacetimedb-update-*` 作为离线交付包。 - 关联文档:`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 ## 2026-05-19 公开 gallery 入口发布限流以快拒绝保护后端 @@ -127,6 +122,38 @@ - 验证方式:容器连续 10 轮不重启 SpacetimeDB 压测,`PEAK_RPS=2500` 等价约 5000 HTTP req/s,平均实际吞吐约 `4219 HTTP req/s`,总计 `0` 个 5xx,200 请求平均 `p95=123ms`、`p99=234ms`;同时观察 SpacetimeDB 内存高水位,后续优化先处理连接 / 订阅 / tracking 下游状态。 - 关联文档:`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`、`deploy/container/README.md`。 +## 2026-05-19 新增玩法创作工具平台 SOP 冻结 + +- 背景:新增玩法的创作工具如果默认复制既有玩法的聊天式 Agent、轻输入 Agent 或专属素材模型,平台会不断复制出不可控分支,后续接入、测试和恢复语义都会漂移。 +- 决策:新增玩法创作工具统一收敛为平台级 SOP:默认使用表单/图片输入创作工作台;单图资产统一通过 `CreativeImageInputPanel`;系列素材统一走批量规划、sheet 生图、后端切图、透明化、OSS 持久化和局部重生成流水线;不把任一玩法专属素材模型当平台通用模型。 +- 影响范围:`CONTEXT.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`、`.codex/skills/genarrative-play-type-integration/SKILL.md`、`.hermes/skills/genarrative-play-type-integration/SKILL.md`、后续新增玩法 PRD 和工程实现。 +- 验证方式:新增玩法 PRD 必须显式声明单图资产槽位和系列素材槽位;新增工作台测试确认没有默认聊天式 Agent 输入;skill 通过 `quick_validate.py`。 +- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`、`.codex/skills/genarrative-play-type-integration/SKILL.md`、`.hermes/skills/genarrative-play-type-integration/SKILL.md`。 + +## 2026-05-21 RPG publish_world 设定文本以后端草稿真相派生 + +- 背景:RPG 结果页发布动作只保证提交 `{ action: 'publish_world' }`;旧 agent 会话可能没有 `seed_text`,但 `draft_profile_json` 已经通过 `publish_gate` 并可发布。 +- 决策:发布正式世界时,`spacetime-module` 不再把 `session.seed_text` 当作唯一 `setting_text` 兜底,而是调用 `module-custom-world::resolve_custom_world_publish_setting_text(...)` 从 payload、当前草稿 profile 和 seed 依次派生。 +- 影响范围:RPG / custom-world agent 发布链路、`custom_world_profile` 编译入库、公开 gallery 投影。 +- 验证方式:`cargo test -p module-custom-world publish_setting_text --manifest-path server-rs\Cargo.toml`;`cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml`;本地 api-server 重启后检查 `/healthz`。 +- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`.hermes/shared-memory/pitfalls.md`。 + +## 2026-05-19 系列素材 n*n 图集抽为 api-server 通用模块 + +- 背景:抓大鹅物品 sheet 已包含 prompt 组装、固定网格切图、绿幕 / 近白底透明化、切片 PNG 持久化和 prompt 追踪;继续留在 Match3D 私有模块会让跳一跳、后续地块 / 道具类玩法重复复制同一套算法和 OSS 元数据口径。 +- 决策:`server-rs/crates/api-server/src/generated_asset_sheets.rs` 作为通用系列素材图集模块,`n` 作为必选 `grid_size` 参数;物品名称 prompt 模板与特殊设定 prompt 作为可选输入;模块负责 sheet prompt、`n*n` 切片、透明化、PNG 输出、OSS private upload 请求构造,以及 sheet / item / special prompt 的 base64 元数据持久化。玩法只负责生图 provider、计费、slot 规划、失败回写和把通用切片结果映射回自身 DTO / 草稿 / runtime 字段。 +- 影响范围:`api-server` 系列素材生成、Match3D 物品五视角素材、后续新增玩法的地块 / 物品 / 障碍 / 装饰图集生成。 +- 验证方式:`cargo test -p api-server generated_asset_sheets --manifest-path server-rs\Cargo.toml -- --nocapture` 覆盖通用 prompt、切片、`n` 校验和 prompt 元数据;玩法侧执行对应素材流水线定向测试。 +- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。 + +## 2026-05-19 跳一跳玩法采用正式 scoring DTO 与 public view 投影 + +- 背景:跳一跳玩法新增后,前端、shared-contracts、SpacetimeDB 生成绑定和后端 mapper 对 scoring 字段口径不一致,schema guard 也要求 table / view 目录与 `migration.rs` 同步。 +- 决策:跳一跳的 `JumpHopScoring` 统一采用 `chargeToDistanceRatio/maxChargeMs/hitBonus/perfectBonus`,公开广场优先使用 `jump_hop_gallery_card_view`,详情兼容投影保留 `jump_hop_gallery_view`。`spacetime-module` 新增的 `jump_hop_*` table 必须同步进入 `migration.rs` 和后端架构文档。 +- 影响范围:`packages/shared/src/contracts/jumpHop.ts`、`server-rs/crates/shared-contracts/src/jump_hop.rs`、`server-rs/crates/spacetime-client/src/mapper/jump_hop.rs`、`server-rs/crates/spacetime-module/src/migration.rs`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。 +- 验证方式:`cargo check -p shared-contracts --manifest-path server-rs/Cargo.toml`、`cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:spacetime-schema`。 +- 关联文档:`docs/prd/【玩法创作】跳一跳俯视角玩法模板PRD-2026-05-19.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。 + ## 2026-05-16 公开作品列表短期由 BFF 订阅读模型缓存 - 背景:作品列表压测和实时性讨论中,曾考虑让浏览器前端直接订阅公开作品列表,减少 HTTP 拉取和 BFF 压力。 @@ -194,6 +221,7 @@ ## 2026-05-14 抓大鹅物品素材 sheet 改用 VectorEngine Gemini +- 状态:历史决策,已被 `2026-05-22 抓大鹅素材生成改为关卡整图派生三图` 取代;当前物品 spritesheet 走 `gpt-image-2` 参考关卡整图编辑生成 `2K 1:1`、`10*10` 绿幕图,上传 OSS 前扣成透明 PNG。 - 背景:抓大鹅 2D 五视角物品素材仍沿用 5x5 sheet、绿幕去背、切图、OSS 转存和 `generatedItemAssets` 持久化,但用户要求物品素材图片生成步骤改用 VectorEngine Apifox `api-381740608` 对应的 Gemini 原生图片接口。 - 决策:抓大鹅物品素材 sheet 生图固定走 VectorEngine `POST {VECTOR_ENGINE_BASE_URL}/v1beta/models/gemini-3-pro-image-preview:generateContent?key={VECTOR_ENGINE_API_KEY}`,请求体使用 `contents[].parts[].text` 与 `generationConfig.responseModalities = ["TEXT", "IMAGE"]`、`imageConfig.aspectRatio = "1:1"`;响应从 `candidates[].content.parts[].inlineData.data` / `inline_data.data` 读取 base64 图片。封面、9:16 纯背景图、1:1 容器 UI 图、切图、OSS、扣费和运行态消费链路保持不变;音频以后续“拼图与抓大鹅音频生成入口临时关闭”决策为准。 - 影响范围:`server-rs/crates/api-server/src/match3d.rs`、`server-rs/crates/api-server/src/config.rs`、`deploy/env/api-server.env.example`、抓大鹅素材生成技术文档。 @@ -245,7 +273,7 @@ ## 2026-05-12 抓大鹅入口素材风格改为 2D 常见素材风格 - 背景:抓大鹅草稿素材生成已经收敛为多视角 2D 图片素材,但入口页和旧参考图仍沿用黏土、低多边形、塑料、木雕、体素、金属等偏 3D 素材语言,容易让后续生成链路和用户预期继续漂移。 -- 决策:抓大鹅创作入口 `2D素材风格` 固定为 `扁平图标 / 赛璐璐卡通 / 像素复古 / 手绘水彩 / 贴纸描边 / 厚涂图标 / 自定义`;默认风格为 `flat-icon`。入口参考图统一由 `npm run assets:match3d-style-references -- --live` 调用 VectorEngine `gpt-image-2-all` 生成,输出到 `public/match3d-style-references/`。旧 3D 风格参考图不再保留为入口资产。 +- 决策:抓大鹅创作入口 `2D素材风格` 固定为 `扁平图标 / 赛璐璐卡通 / 像素复古 / 手绘水彩 / 贴纸描边 / 厚涂图标 / 自定义`;默认风格为 `flat-icon`。入口参考图统一由 `npm run assets:match3d-style-references -- --live` 调用 VectorEngine `gpt-image-2` 生成,输出到 `public/match3d-style-references/`。旧 3D 风格参考图不再保留为入口资产。 - 影响范围:`Match3DAgentWorkspace`、抓大鹅入口交互测试、Match3D PRD、素材生成流水线技术文档、F1 入口文档和 `public/match3d-style-references/` 静态资产。 - 验证方式:执行 `npm run test -- src\components\match3d-creation\Match3DAgentWorkspace.interaction.test.tsx`、`cargo test -p shared-contracts match3d --manifest-path server-rs\Cargo.toml`、`npm run typecheck`、`npm run check:encoding`,并人工抽查 `.tmp/match3d-style-preview.png`。 - 关联文档:`docs/prd/AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`、`docs/technical/MATCH3D_F1_CREATION_ENTRY_AND_AGENT_UI_2026-04-30.md`。 @@ -261,8 +289,9 @@ ## 2026-05-12 拼图 UI 背景图复用 levels_json 持久化 - 背景:拼图草稿结果页需要像抓大鹅一样支持 UI 背景生成,但首版只需要作品级/首关背景,不应为图片生成结果新增 SpacetimeDB 表结构。 -- 决策:拼图 UI 背景字段存入首关 `levels_json`,字段为 `uiBackgroundPrompt`、`uiBackgroundImageSrc`、`uiBackgroundImageObjectKey`;`compile_puzzle_draft` 草稿编译阶段自动生成首关 UI 背景,自动草稿阶段必须拿到 `uiBackgroundImageSrc` 或 `uiBackgroundImageObjectKey` 才能返回成功;结果页新增 `UI` Tab,可编辑提示词并触发 `generate_puzzle_ui_background`,手动生成失败只展示在当前面板。`api-server` 读取 `public/ui-previews/puzzle-image-compact-ui-2026-05-08.png` 作为非拼图 UI 参考图,调用 VectorEngine `gpt-image-2-all` 生成 9:16 背景并要求中央正方形拼图区与外部 UI 背景边界清晰。SpacetimeDB 只保存结果,不做外部 I/O。 +- 决策:拼图 UI 背景字段存入首关 `levels_json`,字段为 `uiBackgroundPrompt`、`uiBackgroundImageSrc`、`uiBackgroundImageObjectKey`;`compile_puzzle_draft` 草稿编译阶段自动生成首关 UI 背景,自动草稿阶段必须拿到 `uiBackgroundImageSrc` 或 `uiBackgroundImageObjectKey` 才能返回成功;结果页新增 `UI` Tab,可编辑提示词并触发 `generate_puzzle_ui_background`,手动生成失败只展示在当前面板。`api-server` 读取 `public/ui-previews/puzzle-image-compact-ui-2026-05-08.png` 作为非拼图 UI 参考图,调用 VectorEngine `gpt-image-2` 生成 9:16 背景并要求中央正方形拼图区与外部 UI 背景边界清晰。SpacetimeDB 只保存结果,不做外部 I/O。 - 2026-05-18 追加:为缩短首版草稿等待,`compile_puzzle_draft` 在首关命名和 `uiBackgroundPrompt` 稳定后并行启动首关关卡图生成与 UI 背景生成;上传主图且关闭 AI 重绘时,并行执行上传图持久化与 UI 背景生成。生成页预计完成时间按 5 分钟展示。 +- 2026-05-21 追加:拼图结果页独立“素材配置”Tab 已移除,UI spritesheet 与关卡纯背景收口到每关图片生成资产包。每次 `gpt-image-2` 预计 90 秒;草稿完整 AI 重绘路径约 298 秒,上传图且关闭 AI 重绘路径跳过首图生成约 208 秒。结果页关卡详情继续复用 `CreativeImageInputPanel`,本次上传/历史选择图优先成为主图卡片,正式图只作为无新参考图时的预览;仅有正式图时仍允许在画面描述框上传多张参考图。 - 影响范围:拼图结果页、拼图运行态背景渲染、拼图 agent action、`module-puzzle` / `spacetime-module` / `spacetime-client` 的拼图关卡 JSON 映射、拼图流程技术文档。 - 验证方式:执行 `npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx`、`cargo test -p api-server puzzle_ui_background --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run typecheck`、`npm run check:encoding`。 - 关联文档:`docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md`。 @@ -270,7 +299,7 @@ ## 2026-05-12 抓大鹅结果页素材编辑统一走作品级资产面板 - 背景:抓大鹅结果页需要支持封面图上传 / AI 重绘、物品素材独立预览、单项删除和批量新增,且不能把素材编辑继续做成列表内联展开或前端临时状态。 -- 决策:结果页 `作品信息` 的封面图点击打开独立面板,封面图面板对齐拼图入口上传卡。已有上传主图时,请求体传 `uploadedImageSrc`,AI 重绘走 VectorEngine `/v1/images/edits`;关闭 AI 重绘时只写回上传图,不调用生图。没有上传主图时,请求体传 `referenceImageSrcs`,可混合本地上传、物品素材和 UI 素材,多参考图作为 `gpt-image-2-all` generations 的 `image` 数组传入。生成结果统一调用 `POST /api/creation/match3d/works/{profileId}/cover-image` 并转存到 `generated-match3d-assets`。`素材配置 > 物品` 列表项点击打开独立预览面板,不再提供单项重新生成按钮;单项删除和批量新增都写回同一份 `generated_item_assets_json`。批量新增调用 `POST /api/creation/match3d/works/{profileId}/item-assets`,复用草稿生成的 2D 素材图、5x5 切图、OSS 上传和可选点击音效链路,仅作用于新增物品,不新增 SpacetimeDB 表。 +- 决策:结果页 `作品信息` 的封面图点击打开独立面板,封面图面板对齐拼图入口上传卡。已有上传主图时,请求体传 `uploadedImageSrc`,AI 重绘走 VectorEngine `/v1/images/edits`,后端把上传图作为 multipart `image` part 传入 `gpt-image-2`;关闭 AI 重绘时只写回上传图,不调用生图。没有上传主图但存在 `referenceImageSrcs` 时,多参考图同样走 edits 的多个 `image` part;完全无参考图时走 `/v1/images/generations`。生成结果统一调用 `POST /api/creation/match3d/works/{profileId}/cover-image` 并转存到 `generated-match3d-assets`。`素材配置 > 物品` 列表项点击打开独立预览面板,不再提供单项重新生成按钮;单项删除和批量新增都写回同一份 `generated_item_assets_json`。批量新增调用 `POST /api/creation/match3d/works/{profileId}/item-assets`;该接口的物品 spritesheet 生成口径已被 2026-05-22 决策更新为关卡整图参考、`10*10` 绿幕图和上传前透明化。 - 影响范围:Match3D 结果页、Match3D works shared contracts、`api-server` Match3D 作品路由、生成资产历史类型和草稿恢复路径。 - 验证方式:执行 `npm run test -- src/components/match3d-result/Match3DResultView.test.tsx`、`npm run typecheck`、`cargo test -p api-server match3d --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:encoding`。 - 关联文档:`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。 @@ -294,7 +323,7 @@ ## 2026-05-13 宝贝爱画先作为寓教于乐独立本地 Demo 落地 - 背景:第三关 `宝贝爱画` 需要默认出现在“发现 / 寓教于乐”板块下方,但本阶段只验证画板、手部绘制、绘画魔法和本地保存闭环,不进入创作模板、公开作品或正式持久化。 -- 决策:`baby-love-drawing / 宝贝爱画` 先作为独立运行态接入,入口由发现页寓教于乐默认卡片打开,并支持 `/runtime/baby-love-drawing` 直达;关闭 `VITE_ENABLE_EDUTAINMENT_ENTRY` 时前端不展示频道/卡片且直达路由回落主应用。绘画魔法统一走 `POST /api/creation/edutainment/baby-love-drawing/magic` 后端安全代理,使用 VectorEngine `gpt-image-2-all` 与原始画布 Data URL 参考图生成绘本风图片;保存只写 localStorage,正式持久化后续再设计。 +- 决策:`baby-love-drawing / 宝贝爱画` 先作为独立运行态接入,入口由发现页寓教于乐默认卡片打开,并支持 `/runtime/baby-love-drawing` 直达;关闭 `VITE_ENABLE_EDUTAINMENT_ENTRY` 时前端不展示频道/卡片且直达路由回落主应用。绘画魔法统一走 `POST /api/creation/edutainment/baby-love-drawing/magic` 后端安全代理,使用 VectorEngine `gpt-image-2` 与原始画布 Data URL 参考图生成绘本风图片;保存只写 localStorage,正式持久化后续再设计。 - 影响范围:`packages/shared/src/contracts/edutainmentBabyDrawing.ts`、`src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.tsx`、`src/services/edutainment-baby-drawing/`、`src/routing/appRoutes.tsx`、`src/components/rpg-entry/RpgEntryHomeView.tsx`、`server-rs/crates/api-server/src/edutainment_baby_drawing.rs`、`src/index.css`、宝贝爱画 PRD 与技术方案。 - 验证方式:执行宝贝爱画 model/runtime/service/route 定向测试、`npm run typecheck`、定向 ESLint、`cargo test -p api-server edutainment_baby_drawing --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server resolves_runtime_paths_to_creation_type_ids --manifest-path server-rs/Cargo.toml` 和编码检查;真实魔法生成需配置 `VECTOR_ENGINE_BASE_URL` 与 `VECTOR_ENGINE_API_KEY`。 - 关联文档:`docs/prd/BABY_LOVE_DRAWING_EDUTAINMENT_LEVEL_PRD_2026-05-13.md`、`docs/technical/BABY_LOVE_DRAWING_RUNTIME_DEMO_IMPLEMENTATION_2026-05-13.md`。 @@ -327,7 +356,7 @@ ## 2026-05-10 儿童动作 Demo 视觉资产统一为绘本草地舞台 - 背景:儿童动作 Demo 需要从暗色科技风切换到更适合儿童互动的卡通绘本草地风格,并且要让背景、地面、UI、地面指示环和用户轮廓使用同一套 image-2 资源口径。 -- 决策:热身舞台及后续儿童动作 Demo 场景、物品、UI 资源统一采用明亮卡通绘本草地视觉语言。真实资源默认输出到 `public/child-motion-demo/`。背景沿用 `picture-book-grass-stage.png`;地面、指示环、角色指示器和 UI 已拆分为用途专属资源:`picture-book-foreground-grass-v2.png`、`picture-book-ground-ring-v3.png`、`picture-book-character-outline-v4.png`、`picture-book-hud-strip-v2.png`、`picture-book-calibration-strip-v2.png`、`picture-book-start-panel-v2.png` 和 `picture-book-ui-button-v2.png`。其中角色指示器 v4 基于 v2 本地后处理为更细的白色描边样式,内部透明,耳朵、手指、脚趾等细节已弱化,页面显示尺寸相对上一版放大 50%。生成脚本固定为 `scripts/generate-child-motion-demo-assets.mjs`,并通过 `npm run assets:child-motion-demo` 调用 VectorEngine `gpt-image-2-all`;透明资源使用品红底生成后本地去背,中间源图仅保存在 `tmp/child-motion-demo-assets/`。在缺少 `VECTOR_ENGINE_BASE_URL` 或 `VECTOR_ENGINE_API_KEY` 时,只允许 dry-run 和 CSS 兜底,不伪造 live 生图结果。 +- 决策:热身舞台及后续儿童动作 Demo 场景、物品、UI 资源统一采用明亮卡通绘本草地视觉语言。真实资源默认输出到 `public/child-motion-demo/`。背景沿用 `picture-book-grass-stage.png`;地面、指示环、角色指示器和 UI 已拆分为用途专属资源:`picture-book-foreground-grass-v2.png`、`picture-book-ground-ring-v3.png`、`picture-book-character-outline-v4.png`、`picture-book-hud-strip-v2.png`、`picture-book-calibration-strip-v2.png`、`picture-book-start-panel-v2.png` 和 `picture-book-ui-button-v2.png`。其中角色指示器 v4 基于 v2 本地后处理为更细的白色描边样式,内部透明,耳朵、手指、脚趾等细节已弱化,页面显示尺寸相对上一版放大 50%。生成脚本固定为 `scripts/generate-child-motion-demo-assets.mjs`,并通过 `npm run assets:child-motion-demo` 调用 VectorEngine `gpt-image-2`;透明资源使用品红底生成后本地去背,中间源图仅保存在 `tmp/child-motion-demo-assets/`。在缺少 `VECTOR_ENGINE_BASE_URL` 或 `VECTOR_ENGINE_API_KEY` 时,只允许 dry-run 和 CSS 兜底,不伪造 live 生图结果。 - 影响范围:`src/index.css`、`src/components/child-motion-demo/ChildMotionWarmupDemo.tsx` 的舞台视觉层、儿童动作 Demo 技术文档、后续 image-2 资产生成流程。 - 验证方式:检查 `/child-motion-demo` 舞台是否在未生成资产时仍有可用草地绘本兜底;补齐 VectorEngine 私密配置后运行 `npm run assets:child-motion-demo -- --live` 或 `--live --only ` 应能写出对应 PNG,并确认页面静态资源返回 `image/png`。若只调整透明去背、裁切或品红边缘,可运行 `npm run assets:child-motion-demo -- --live --postprocess-only --force --only ` 复用源图后处理。页面接入时必须按资源原始比例等比使用,不得把方形软纸面板拉伸成 HUD、状态条或底部草坪。 - 关联文档:`docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md`、`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md`。 @@ -348,6 +377,14 @@ - 验证方式:执行入口配置、创作 Hub、平台入口交互和 api-server 路由熔断定向测试,确认“视觉小说”不出现在创作页且 `/api/creation/visual-novel/*` 默认被熔断。 - 关联文档:`docs/design/PLATFORM_CREATE_TAB_CREATIVE_AGENT_HOME_2026-05-05.md`、`docs/technical/ADMIN_CREATION_ENTRY_SWITCH_CONFIG_2026-05-11.md`。 +## 2026-05-20 RPG 创作入口开放 + +- 背景:RPG 文字冒险能力已经具备历史 custom-world 创作和运行闭环,但入口默认种子仍 `visible=false`,创作页不展示。 +- 决策:SpacetimeDB `creation_entry_type_config` 默认种子中 `rpg.visible=true` 且 `open=true`,旧默认隐藏配置只在标题、subtitle、badge、图片、排序和开关完全匹配时迁移为可见可创建。`airp` 仍保持 AI RPG 占位,不接管当前 RPG 链路。结构化创作 / RPG JSON 链路默认关闭 Responses `web_search`,需要联网增强时才通过 `GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED=true` 或 `GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED=true` 显式启用;未开通工具的上游会返回 `ToolNotOpen`,不能把这类失败暴露成“模型返回结果解析失败”。 +- 影响范围:创作入口默认种子、旧库入口纠偏、`api-server` 入口熔断、创作页模板 Tab、创作 Hub 测试、玩法链路文档和后端路由文档。 +- 验证方式:执行入口配置、api-server 路由熔断、创作 Hub 和平台入口交互定向测试,确认“文字冒险”出现在创作入口,`/api/runtime/custom-world*`、`/api/story/*`、`/api/runtime/chat/*` 都按 `rpg` 入口开关熔断。 +- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。 + ## 2026-05-10 运行态输入设备抽象层全项目通用化 - 背景:拼图运行态接入 mocap 后,鼠标/触控和 mocap 各自维护输入逻辑会导致合并大块、拖拽语义和取消会话行为不一致;后续其他玩法也需要复用体感、摇杆、键盘等设备输入。 @@ -375,11 +412,19 @@ ## 2026-05-09 GPT-image-2 图片生成统一迁移到 VectorEngine - 背景:仓库内 RPG、拼图、方洞和本地模板脚本的 GPT-image-2 生图此前依赖 APIMart 图片网关;团队要求参考 VectorEngine Apifox `api-448710071`,后续不再使用 APIMart 执行 GPT-image-2 图片生成。 -- 决策:所有 GPT-image-2 生图请求统一走 VectorEngine `POST /v1/images/generations`,基础配置读取 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` / `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`,上游模型使用 `gpt-image-2-all`,请求体不再携带 `official_fallback`,参考图字段改为 `image`。APIMart 只保留给创意 Agent 的 `gpt-5` Responses 文本/多模态链路。 +- 决策:所有 GPT-image-2 无参考图生图请求统一走 VectorEngine `POST /v1/images/generations`,有参考图请求走 `POST /v1/images/edits` multipart,基础配置读取 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` / `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`,上游模型使用 `gpt-image-2`,请求体不再携带 `official_fallback`。APIMart 只保留给创意 Agent 的 `gpt-5` Responses 文本/多模态链路。 - 影响范围:`api-server` 共享图片 helper、拼图图片生成、角色主图、RPG 场景图、开局 CG 故事板、方洞视觉资产、生产环境示例、gpt-image-2 本地 skill 和相关技术文档。 - 验证方式:执行 `npm run check:encoding`、`cargo test -p api-server openai_image --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server puzzle --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server custom_world_ai --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server character_visual --manifest-path server-rs/Cargo.toml`,并用 `npm run dev:api-server` + `/healthz` 做后端 smoke。 - 关联文档:`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md`、`docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md`。 +## 2026-05-21 GPT-image-2 参考图统一走 edits multipart + +- 背景:VectorEngine Apifox 创建 `api-446794806` 与编辑 `api-446794807` 明确区分无参考图创建和有参考图编辑;仓库旧实现曾把参考图塞入 `gpt-image-2` generations 的 `image` 数组,导致与供应商当前契约不一致。 +- 决策:所有 GPT-image-2 无参考图生成调用 `POST /v1/images/generations`,所有有参考图生成调用 `POST /v1/images/edits`,模型固定 `gpt-image-2`,参考图作为 multipart `image` part 传入;仓库不再调用 `gpt-image-2-all`。 +- 影响范围:`api-server` 共享图片 helper、拼图图片生成、Match3D 封面重绘和容器 UI 图、gpt-image-2 本地 skill、玩法链路文档和后端架构文档。 +- 验证方式:搜索仓库不应再出现 VectorEngine 图片编辑路径调用;执行 `cargo test -p api-server openai_image_generation --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server puzzle_vector_engine --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server match3d_background --manifest-path server-rs/Cargo.toml`。 +- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。 + ## 2026-05-08 Hyper3D Rodin Gen-2 只通过后端安全代理接入 - 背景:需要接入 Hyper3D Rodin Gen-2 的文生 3D 模型与图生 3D 模型,但供应商 API Key 不能进入前端、文档或 Git;本次只是外部副作用代理,不需要新增平台真相表。 @@ -553,7 +598,7 @@ ## 2026-05-10 视觉小说入口收敛为单句创作 + 画风选择 - 背景:视觉小说入口页要对齐抓大鹅式的线性创作入口,只保留最小可用输入,避免再暴露文档 / 空白 / 对话式工作台。 -- 决策:入口页只展示一句话创作输入框和横向视觉画风卡片;画风通过 `seedText` 追加 `视觉画风` 和 `画风要求` 两行透传给既有创作链路;点击生成后先进入 `visual-novel-generating` 过程页,再自动进入 `visual-novel-result`。画风卡片主视觉固定消费 `public/visual-novel-style-references/` 下由 VectorEngine `gpt-image-2-all` 生成的静态参考图,不在前端运行时现场调用生图接口。 +- 决策:入口页只展示一句话创作输入框和横向视觉画风卡片;画风通过 `seedText` 追加 `视觉画风` 和 `画风要求` 两行透传给既有创作链路;点击生成后先进入 `visual-novel-generating` 过程页,再自动进入 `visual-novel-result`。画风卡片主视觉固定消费 `public/visual-novel-style-references/` 下由 VectorEngine `gpt-image-2` 生成的静态参考图,不在前端运行时现场调用生图接口。 - 影响范围:`VisualNovelAgentWorkspace`、`visualNovelEntryGeneration`、`PlatformEntryFlowShellImpl`、视觉小说 PRD 和创作 Tab 设计文档;不新增后端字段或数据库结构。 - 验证方式:执行 `npm run test -- VisualNovelAgentWorkspace`、视觉小说工作台相关 ESLint、`npx prettier --check` 和 `npm run check:encoding`;`npm run typecheck` 若失败需先区分是否来自无关 Match3D / RPG 既有改动。 - 关联文档:`docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md`、`docs/design/PLATFORM_CREATE_TAB_CREATIVE_AGENT_HOME_2026-05-05.md`。 @@ -577,7 +622,7 @@ ## 2026-05-12 抓大鹅物品种类从消除次数中拆出并改为 2D 五视角素材 - 背景:结果页草稿素材已经能生成和预览,但标准 / 硬核难度仍可能按 `clearCount` 误判需要 12 / 20 种素材,且继续生产 GLB 会拉长草稿生成耗时。 -- 决策:难度配置统一使用 `物品种类`:轻松 3、标准 9、进阶 15、硬核 21;历史硬核 `clearCount=20` 在运行态升为 21 组三消。新草稿和批量新增不再调用 Rodin、不再生成 GLB。每个物品生成 5 个不同 2D 视角,单张 1K 素材图固定按 5x5 切割,最多承载 5 个物品;超过 5 个物品时由 `api-server` 自动分批并行生图。发布必须校验已生成 `image_ready` 且有 `imageViews[]` 或首图引用的素材数量满足当前难度;试玩通过 `itemTypeCountOverride` 自动降到可用 2D 素材数量。历史模型字段只作为旧数据兼容,不再进入新生产链路。 +- 决策:难度配置统一使用运行态 `物品种类`:轻松 3、标准 9、进阶 15、硬核 20;历史硬核 `clearCount=20` 在运行态仍升为 21 组三消,但类型池最多 20 种。新草稿和批量新增不再调用 Rodin、不再生成 GLB。每次固定从 `2K 1:1`、`10*10` 物品 spritesheet 解析并持久化 20 个物品、每个 5 个不同 2D 形态,物品信息列表全部展示 20 个;持久化行列索引按每行两种物品计算,不能超过 `1..=10`。发布必须校验已生成 `image_ready` 且有 `imageViews[]`、首图引用或可解析的物品 spritesheet 满足当前难度;试玩通过 `itemTypeCountOverride` 自动降到可用 2D 素材数量。历史模型字段只作为旧数据兼容,不再进入新生产链路。 - 影响范围:Match3D 结果页、运行态启动契约、`module-match3d` 初始 run 生成、SpacetimeDB start input / restart、发布校验和 Match3D 技术文档。 - 验证方式:`npm run test -- src/components/match3d-result/Match3DResultView.test.tsx`、`cargo test -p module-match3d --manifest-path server-rs\Cargo.toml`、相关后端 check / tests。 - 关联文档:`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。 @@ -646,6 +691,14 @@ - 验证方式:server provision 跑过后,目标机应同时具备 Brotli 模块包与 `nginx -t` 可接受的 brotli 指令;再由 Nginx 模板启用对应指令。 - 关联文档:`deploy/nginx/README.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 +## 2026-05-19 server provision 下载件固定由 Windows 节点断点续传 + +- 背景:`SpacetimeDB` 和 `otelcol-contrib` release 资产在 Linux 目标机直接下载很慢;改到 Windows Jenkins 节点下载后,GitHub 大文件仍可能出现 `curl: (18)` 响应体截断。 +- 决策:`Genarrative-Server-Provision` 的 `Download Provision Tool Archives` 阶段继续只在 Windows 节点下载,再通过 `stash/unstash` 交给目标 Linux agent;下载前查 GitHub release asset `digest`,本地最终文件 SHA256 命中即跳过,`.download` 临时文件用于 `curl -C -` 断点续传,完整返回但 digest 不匹配才清理重下。 +- 影响范围:`jenkins/Jenkinsfile.production-server-provision`、目标机 `scripts/prepare-server-provision-tools.sh` 的本地下载件消费路径、生产 provision 运维排障。 +- 验证方式:Windows 下载日志应出现 digest 查询、已存在校验跳过或 `curl 断点续传`;Linux 目标机阶段只使用 `provision-tool-downloads/` 中的 tarball,不访问 GitHub 下载地址。 +- 关联文档:`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 + ## 个人任务与埋点首版边界冻结 - 背景:“我的”Tab、任务、奖励、钱包和埋点涉及用户、运营、分析多条链路,需要避免范围泛化。 diff --git a/.hermes/shared-memory/development-workflow.md b/.hermes/shared-memory/development-workflow.md index f1268ce4..bb3daa40 100644 --- a/.hermes/shared-memory/development-workflow.md +++ b/.hermes/shared-memory/development-workflow.md @@ -112,6 +112,24 @@ SpacetimeDB bindings 生成: npm run spacetime:generate ``` +CodeGraph 本地语义索引: + +```bash +npm run codegraph:init +npm run codegraph:status +npm run codegraph:sync +npm run codegraph:index +``` + +`.codegraph/config.json` 可随仓库共享;`.codegraph/codegraph.db`、缓存和日志为本机生成物,不提交。 + +Codex 项目级 hook 保存在 `.codex/config.toml` 与 `.codex/hooks/`: + +- `PreToolUse` hook:`node .codex/hooks/pre-submit-compile-check.mjs`,Codex 准备执行 `git commit` 前检查 `npm run typecheck`、`npm run admin-web:typecheck`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`。 +- `PostToolUse` hook:`node .codex/hooks/post-edit-codegraph-sync.mjs`,工具修改文件后执行 `npm run codegraph:sync`。 + +个人 token、模型路由、MCP server 仍属于个人环境;需要时由成员本机执行 `codegraph install` 或查看 `codegraph install --print-config codex`,不要提交个人全局配置。 + ## 常用检查命令 - 后端通用用户行为埋点统一通过 `record_tracking_event_and_return` procedure、`SpacetimeRuntimeClient::record_tracking_event(...)` 与 api-server `tracking` 中间件写入 `tracking_event` / `tracking_daily_stat`;后台、RPG、大鱼吃小鱼、Visual Novel、Story、Combat 默认排除;作品级游玩埋点统一使用 `work_play_start`,详细事件清单见 `docs/technical/BACKEND_TRACKING_EVENT_COVERAGE_2026-05-09.md`。 diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index 6f3cf2ef..82dfa7a8 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -14,6 +14,32 @@ - 关联:相关文件、文档、提交或 Issue ``` +## 抓大鹅新 UI spritesheet 不要回退成中心容器图 + +- 现象:新素材流程生成后,运行态棋盘中心可能叠出一整张 UI spritesheet,导致按钮素材、方格和空白图集覆盖容器区域。 +- 原因:为了兼容旧 DTO,后端可能把 `uiSpritesheetImage*` 同步写入历史 `containerImage*` 字段;旧前端只看 `containerImage*`,会误把 UI 图集当透明中心容器。 +- 处理:读取中心容器图时先比较归一化后的 `containerImage*` 与 `uiSpritesheetImage*`。两者同源时忽略 `containerImage*`,只把它作为旧数据兼容字段;新流程背景图本身已经保留容器,运行态只需加载背景和解析 UI / 物品 spritesheet。 +- 验证:`npm run test -- src/components/match3d-runtime/Match3DRuntimeShell.test.tsx` 应覆盖“运行态不把兼容写入的UI spritesheet当中心容器图”。 +- 关联:`src/components/match3d-runtime/Match3DRuntimeShell.tsx`、`server-rs/crates/api-server/src/match3d/mappers.rs`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + +## UI spritesheet 不要依赖模型直接生成透明背景 + +- 现象:拼图或抓大鹅运行态解析 UI spritesheet 时,把整张背景图、棋盘格、叶子或装饰图也当作 UI 素材区域,按钮映射错乱;截图里常表现为底部按钮区只剩透明棋盘格或素材碎片。 +- 原因:前端解析依赖 alpha 连通域检测,透明背景是前提;但生图模型收到“透明背景 spritesheet”提示后仍可能输出带实景背景或伪透明棋盘格的普通不透明 PNG,OSS 中保存的图没有真实 alpha。 +- 处理:UI spritesheet 提示词应要求统一纯绿色绿幕背景,而不是让模型直接产透明背景;后端在上传 OSS 前复用 `generated_asset_sheets::apply_generated_asset_sheet_green_screen_alpha(...)` 把绿幕扣成真实透明 PNG,再把透明图写入 `uiSpritesheetImageSrc/uiSpritesheetImageObjectKey`。 +- 验证:`cargo test -p api-server puzzle_ui_spritesheet_postprocess_turns_green_screen_transparent --manifest-path server-rs\Cargo.toml`、`cargo test -p api-server puzzle_level_scene_spritesheet_and_background_requests_use_references --manifest-path server-rs\Cargo.toml`、`cargo test -p api-server match3d_derived_asset_prompts_match_three_sheet_pipeline --manifest-path server-rs\Cargo.toml`。 +- 关联:`server-rs/crates/api-server/src/puzzle/generation.rs`、`server-rs/crates/api-server/src/match3d/works.rs`、`server-rs/crates/api-server/src/generated_asset_sheets.rs`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + +## 拼图 UI spritesheet 运行态不要二次包圆底或拉伸比例 + +- 现象:拼图运行态左上返回和右上设置按钮外面出现白色圆圈;底部“提示 / 原图 / 冻结”三枚素材被压扁、拉宽或拉成正圆,和图集原始按钮比例不一致。 +- 原因:UI spritesheet 已经包含按钮视觉本体,但运行态仍给顶部按钮套默认圆形 icon 容器;底部三枚素材用 `h-full w-full rounded-full` 铺满按钮格,覆盖了自动检测矩形的真实宽高比。 +- 处理:有 `uiSpritesheetImage*` 时,顶部返回 / 设置按钮容器只保留透明点击区和 focus 状态,不再叠加默认圆形底;`buildPuzzleUiSpriteBackgroundStyle(...)` 对检测到的矩形写入 `aspectRatio`,底部三枚素材按原始宽高比和最大尺寸渲染,不强制 `w-full`。 +- 验证:`npm run test -- src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx`、`npm run test -- src/services/puzzle-runtime/puzzleUiSpritesheetParser.test.ts`。 +- 关联:`src/components/puzzle-runtime/PuzzleRuntimeShell.tsx`、`src/services/puzzle-runtime/puzzleUiSpritesheetParser.ts`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + +2026-05-22 补充:展示矩形和点击热区要分开处理。`puzzleUiSpritesheetParser` 的 `regions` 保留完整视觉裁切矩形,`hitRegions` 用较高 alpha 阈值只包住实心按钮主体;运行态底部 spritesheet 道具按钮启用 `puzzle-runtime-sprite-tool-button--precise-hit`,父按钮不吃整块透明留白,内部 `puzzle-runtime-ui-sprite-hit-zone` 才接收指针事件,避免透明区域成为点击热区。 + ## 图像输入组件不要把业务状态藏在页面内联实现里 - 现象:拼图页把参考图上传、缩略图、主图删除确认和 AI 重绘开关内联实现后,后续想复用到其它创作页时,页面级状态和通用 UI 状态混在一起,容易出现多套上传卡和参考图展示口径。 @@ -22,6 +48,62 @@ - 验证:拼图入口测试仍可通过,且新组件可通过不同页面复用而不需要复制上传卡实现。 - 关联:`src/components/common/CreativeImageInputPanel.tsx`、`src/components/puzzle-agent/PuzzleAgentWorkspace.tsx`。 +## RPG 发布不能只依赖 agent session seed_text + +- 现象:RPG 结果页 `publish_world` 返回 `UPSTREAM_ERROR`,details 为 `custom_world.setting_text 不能为空`;同一 session 的 `result-view` 日志显示 `publish_ready=true`。 +- 原因:前端发布动作只提交 `{ action: 'publish_world' }`,旧 agent 会话的 `seed_text` 可能为空;如果后端只从 action payload 或 `seed_text` 取 `setting_text`,就会在最终 compile / publish 校验阶段失败。 +- 处理:`module-custom-world::resolve_custom_world_publish_setting_text(...)` 以当前 `draft_profile_json` 为草稿真相,优先读取 `settingText`、`creatorIntent.rawSettingText`、`creatorIntent.worldHook`、`worldHook`、`anchorContent.worldPromise(.hook)`、`summary`、`name/title`,最后才回退 `seed_text`。 +- 验证:`cargo test -p module-custom-world publish_setting_text --manifest-path server-rs\Cargo.toml`;`cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml`。 +- 关联:`server-rs/crates/module-custom-world/src/application.rs`、`server-rs/crates/spacetime-module/src/custom_world.rs`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + +## RPG 已发布结果页进入世界不能重复 publish_world + +- 现象:RPG 草稿发布成功后,按钮文案已变为“进入世界”,但点击仍请求 `POST /api/runtime/custom-world/agent/sessions/{sessionId}/actions` 且 payload 为 `{"action":"publish_world"}`,后端返回 `publish_world is only available during object_refining, visual_refining, long_tail_review or ready_to_publish`。 +- 原因:按钮文案依据 agent session `stage === 'published'` 切换,但点击处理仍走发布协调路径;如果前端只依赖草稿同步回包判断是否已发布,回包为空或缺少可进入状态时就会继续重复发送 `publish_world`。 +- 处理:进入世界协调器接收当前 agent session stage;当 stage 已为 `published` 时,只调用 `result-view` 回读已发布 profile 并启动运行态,不再调用 `sync_result_profile` 或 `publish_world`。 +- 验证:`npm run test -- src/components/rpg-entry/useRpgCreationEnterWorld.test.tsx`;确认已发布场景下 `syncAgentDraftResultProfile` 与 `executePublishWorld` 均未被调用。 +- 关联:`src/components/rpg-entry/useRpgCreationEnterWorld.ts`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + +## RPG 点击启动黑屏 / 默认 profile 先查 profile 归一化和摘要覆盖 + +- 现象:作品详情点击“启动”后页面切到 RPG runtime,但用户只看到黑屏、空白,或进入默认角色 / 默认 profile;从作品详情点“作品编辑”后开局 CG、封面、角色图、技能动作预览、初始物品图标或场景背景图丢失;DevTools 里可能同时看到旧自动存档 `/api/runtime/save/snapshot` 被主动 cancel。 +- 原因:`/custom-world-library` / `/custom-world-gallery` 详情接口可能返回历史或摘要式 `profile`,缺少 `playableNpcs`、`storyNpcs`、`landmarks`、`attributeSchema` 等运行态字段;前端 client 若直接把该对象传给 runtime,角色选择首屏会在 `buildCustomWorldPlayableCharacters(profile)` 或后续属性解析处抛错。另一类常见原因是详情接口已回读完整 profile 后,`savedCustomWorldEntries` 里的列表摘要又把 `selectedDetailEntry` 覆盖回空 profile,导致启动或编辑时只剩卡片摘要。发布 / 回读 result-view 若返回字段更少的旧视图,也可能把当前结果页已编辑资产降级掉。`save/snapshot (canceled)` 通常是切 runtime 或卸载时 `AbortController` 取消旧自动存档,不是黑屏根因。 +- 处理:RPG 入口作品库 client 在所有返回 `CustomWorldLibraryEntry` 的接口边界统一调用 `normalizeCustomWorldProfileRecord`,并用 `profileId/worldName/subtitle/summaryText` 补齐旧数据缺字段;详情页已拿到运行态字段或资产槽位更多的完整 profile 时,不允许列表摘要覆盖当前详情;同一 `profile.id` 下,正式进入世界发布 / 回读不得用字段更少的后端旧视图降级当前结果页 profile。`normalizeCustomWorldProfileRecord` 必须近似无损保留 `cover`、`openingCg`、`camp.narrativeResidues`、`landmark.visualDescription/narrativeResidues`、`skills[].actionPreviewConfig`、`initialItems[].iconSrc`、`attributeSchema`、角色 `attributeProfile` 和 `sceneChapterBlueprints[].acts[]` 的背景与结构字段;只有背景资产的 act 也不能被过滤。角色选择页对角色生成异常或空数组回退默认角色,并保留返回按钮/轻量空态;顶层 runtime 懒加载 fallback 不使用纯 `null`。 +- 验证:`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "creation hub published work start uses loaded detail profile instead of library summary|creation hub published work edit keeps loaded detail profile assets instead of library summary"`;`npm run test -- src/data/customWorldLibrary.test.ts -t "保留结果页封面和关键图片资产槽位|近似无损保留编辑态和运行态结构字段|保留只有背景资产的场景幕"`;`npm run test -- src/components/rpg-entry/useRpgEntryAgentDraftRestore.test.tsx -t "默认封面和角色编辑结构差异也不能被列表摘要覆盖"`;`npm run test -- src/components/rpg-entry/useRpgCreationEnterWorld.test.tsx -t "正式进入世界回读结果页字段更少时不降级当前完整 profile"`;`npm run typecheck`。 +- 关联:`src/components/rpg-entry/useRpgEntryLibraryDetail.ts`、`src/components/rpg-entry/useRpgCreationEnterWorld.ts`、`src/data/customWorldLibrary.ts`、`src/services/rpg-entry/rpgEntryLibraryClient.ts`、`src/components/rpg-entry/RpgEntryCharacterSelectView.tsx`、`src/App.tsx`、`src/components/rpg-runtime-shell/RpgRuntimeShell.tsx`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + +## RPG 战后一轮战斗后卡在观察/试探/调息先查 post-battle finalization + +- 现象:RPG 一轮战斗胜利后,运行态只显示默认 `观察周围迹象 / 主动出声试探 / 原地调息`,这些按钮只有文字反馈;点“继续冒险”后又回到同样选项,点探索只播退场/进场动画,场景和剧情不推进。 +- 原因:终局战斗 action 如果只走通用 `resolve_story_runtime_action` fallback,而没有在后端调用 `finalize_post_battle_resolution(...)`,就不会持久写入 `story_continue_adventure`、`deferredOptions` 和下一幕 `currentSceneActState`。另外旧 bootstrap 快照可能只有 `connectedSceneIds` / `forwardSceneId`、没有 `connections`,战后选项生成若只读 `connections` 也会退回 `idle_explore_forward` 循环。 +- 处理:`module-runtime-story` 在 story action 投影后统一调用 post-battle finalization;`idle_explore_forward` 清理战斗态并生成下一段遭遇预览;`idle_travel_next_scene` / `camp_travel_home_scene` 由后端写入新 `currentScenePreset`、场景 act 状态、遭遇预览和 `runtimeStats.scenesTraveled`。前端只负责播放继续、探索和切场景动画,不承接正式剧情推进真相。 +- 验证:`cargo test -p module-runtime-story --manifest-path server-rs\Cargo.toml battle_tests -- --nocapture` 应覆盖战斗终局持久化 `story_continue_adventure`、`deferredOptions`、下一幕 act,以及 `idle_travel_next_scene` 真正切换场景。 +- 关联:`server-rs/crates/module-runtime-story/src/session_action.rs`、`server-rs/crates/module-runtime-story/src/post_battle.rs`、`server-rs/crates/module-runtime-story/src/battle_tests.rs`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + +## RPG 战斗飘字不要只靠低对比红绿文字 + +- 现象:暗色或棕黑噪声背景下,战斗伤害飘字看起来像背景纹理,尤其是远端敌人头顶的小号红字几乎不可读。 +- 原因:旧 `CombatFloatingNumber` 主要依赖 `text-rose-200` / `text-emerald-200` 和 8px 同色 glow;在暗红、棕黑、像素噪声背景上,颜色与背景混在一起,1px 深色描边也不足以形成轮廓。 +- 处理:飘字本体使用高亮近白文字、小面积半透明深色底、明显深色描边和多层黑色阴影;只增强瞬时反馈,不新增说明面板,不遮挡主要战斗画面。 +- 验证:`npm run test -- src/components/game-canvas/GameCanvasEntityLayer.test.tsx` 覆盖伤害/治疗飘字样式策略;运行态截图中敌方头顶伤害数字应能在暗场景上辨认。 +- 关联:`src/components/game-canvas/GameCanvasEntityLayer.tsx`、`docs/【项目基线】当前产品与工程约束-2026-05-15.md`。 + +## 弹窗里复用 CreativeImageInputPanel 要保留画面卡高度 + +- 现象:拼图草稿结果页的关卡详情弹窗中仍能看到“画面图”标题、画面描述和生成按钮,但实际画面图卡片视觉上消失。 +- 原因:`CreativeImageInputPanel` 内部依赖 `flex-1`、`h-full` 和 `max-h-full` 撑开正方形画面卡;放进弹窗里的普通 `section` 后,父级没有可计算高度,卡片会被压到不可见。 +- 处理:通用画面卡 `puzzle-image-upload-card` 保持 `aspect-square` 的同时设置稳定 `min-height`,让入口页和关卡详情弹窗都能显示主图/上传区。 +- 验证:`npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx -t "opens an independent level detail dialog"` 应断言关卡详情中的 `.puzzle-image-upload-card` 具备最小高度类;`npm run test -- src/components/common/CreativeImageInputPanel.test.tsx` 应继续通过。 +- 关联:`src/components/common/CreativeImageInputPanel.tsx`、`src/components/puzzle-result/PuzzleResultView.tsx`、`src/components/puzzle-result/PuzzleResultView.test.tsx`。 + +## Windows provision 下载截断要断点续传而不是回退目标机下载 + +- 现象:`Genarrative-Server-Provision` 在 `Download Provision Tool Archives` 阶段出现 `curl: (18) end of response ... bytes missing`,常见于 `otelcol-contrib_0.151.0_linux_amd64.tar.gz` 等 GitHub release 大文件。 +- 原因:这是 Windows Jenkins 节点到 GitHub 的响应体被截断;若每轮都删除 `.download` 临时文件,就会丢掉已下载部分,下一次又从头开始。 +- 处理:Windows 下载函数保留 `${Output}.download`,`curl` 失败时下一轮使用 `-C -` 断点续传;最终只以 GitHub release asset 的 SHA256 `digest` 作为放行条件,完整返回但 digest 不匹配才删除临时文件重新下载。不要把 SpacetimeDB 或 `otelcol-contrib` 下载挪回 Linux 目标机。 +- 验证:日志应显示 `curl 断点续传 ... resumeBytes=...`,最终出现 `已下载 ... bytes=...`;目标 Linux 阶段只消费 `stash/unstash` 带过去的下载件。 +- 关联:`jenkins/Jenkinsfile.production-server-provision`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 + ## OTLP 端点只填 Collector HTTP base endpoint - 现象:生产或容器 env 里把 `OTEL_EXPORTER_OTLP_ENDPOINT` 填成 `4317`、Rider 端口或别的非 HTTP base endpoint 后,api-server 发不出 OTLP,或者链路被错误转发。 @@ -38,6 +120,30 @@ - 验证:普通 route 请求在 SpacetimeDB 不可用时仍能返回,恢复后 sealed 文件会继续被清理。 - 关联:`server-rs/crates/api-server/src/tracking_outbox.rs`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 +## release tracking outbox 权限错误先查 env 缺失 + +- 现象:release 机器 `journalctl -u genarrative-api.service` 每秒刷 `tracking outbox 定时封存 active 文件失败 error=Permission denied (os error 13)` 和 `tracking outbox 批量写入 SpacetimeDB 失败`。 +- 原因:旧 `/etc/genarrative/api-server.env` 没有 `GENARRATIVE_TRACKING_OUTBOX_DIR` 时,api-server 会回退到本地开发默认相对路径 `server-rs/.data/tracking-outbox`;systemd 工作目录是只读发布目录 `/opt/genarrative/releases/`,`genarrative` 用户不能在其中创建 `server-rs`。 +- 处理:补齐 `GENARRATIVE_TRACKING_OUTBOX_DIR=/var/lib/genarrative/tracking-outbox` 及 batch/flush/max 配置,创建并授权 `/var/lib/genarrative/tracking-outbox` 给 `genarrative:genarrative`,再重启 `genarrative-api.service`。Server-Provision 与 API-Deploy 会保留旧 env 但自动补缺这些运行态路径。 +- 验证:`tr '\0' '\n' < /proc/$(systemctl show genarrative-api.service -p MainPID --value)/environ | grep GENARRATIVE_TRACKING_OUTBOX_DIR` 应指向 `/var/lib/genarrative/tracking-outbox`;重启后当前 PID 不再出现 `Permission denied (os error 13)`。 +- 关联:`scripts/deploy/production-api-deploy.sh`、`scripts/jenkins-server-provision.sh`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 + +## 外部 API 失败没法追溯先查 external_api_call_failure + +- 现象:VectorEngine 图片生成 / 编辑接口对前端只表现为 `502` / `504` 或“上游服务请求失败”,但难以区分是请求发送失败、上游 429/5xx、响应解析失败、未返回图片,还是下载图片失败。 +- 原因:外部 API 失败如果只靠普通日志,不一定能和 OTLP 指标、trace 与 SpacetimeDB 历史查询稳定关联;重启后也容易丢失上下文。 +- 处理:先查 OTLP 指标 `genarrative.external_api.failures{provider,failure_stage,status_class,retryable}`,再查 `tracking_event` 中 `event_key = 'external_api_call_failure'` 的 `metadata_json`。当前通用 VectorEngine `gpt-image-2-all` 适配器会记录 provider、endpoint、operation、failureStage、statusCode、statusClass、timeout、retryable、errorMessage、latencyMs、promptChars、referenceImageCount、imageModel 和 rawExcerpt。 +- 验证:`SELECT event_id, scope_id AS provider, metadata_json, occurred_at FROM tracking_event WHERE event_key = 'external_api_call_failure' ORDER BY occurred_at DESC LIMIT 50;`;如果查不到同时看 tracking outbox 目录权限和 sealed 文件是否堆积。 +- 关联:`server-rs/crates/api-server/src/external_api_audit.rs`、`server-rs/crates/api-server/src/openai_image_generation.rs`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 + +## release 创作接口 413 先查是否还在提交 Data URL + +- 现象:release 上 `POST /api/runtime/puzzle/agent/sessions/{session_id}/actions` 携带参考图 Data URL 时返回 `413 Request Entity Too Large`,access log 显示 `request_time=0.000`、`upstream_status=-`。 +- 原因:Nginx 默认 `client_max_body_size` 只有 1 MiB,请求在反代层被拒绝,根本没有到达 `api-server`;即使模板放宽到 `64m`,把图片 base64 放进创作 JSON body 仍会放大请求体并把上限问题推给下一层。 +- 处理:长期修复不是继续调大 Nginx,而是让浏览器先走 `/api/assets/direct-upload-tickets` 直传 OSS,再 `/api/assets/objects/confirm` 确认 `asset_object`,拼图 action 只提交 `referenceImageAssetObjectId(s)`;后端校验 owner / bucket / kind / MIME / size 后签只读 URL 给 VectorEngine。Nginx `client_max_body_size 64m` 只保留为旧客户端和兼容输入兜底,发布后仍需 `nginx -t && nginx -s reload`。 +- 验证:前端 action payload 不应再出现大段 `data:image/...;base64`;`nginx -T 2>/dev/null | grep client_max_body_size` 可确认反代兜底;再次提交参考图时 access log 应有正常 `upstream_status`,后端测试 `puzzle_reference_image_sources_prefer_asset_object_ids` / `puzzle_asset_object_reference_requires_matching_owner` 应通过。 +- 关联:`src/services/puzzle-works/puzzleAssetClient.ts`、`server-rs/crates/api-server/src/puzzle/vector_engine.rs`、`deploy/nginx/genarrative.conf`、`deploy/nginx/genarrative-dev-http.conf`、`deploy/container/nginx.conf`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 + ## 汪汪声浪入口不要再回到独立配置阶段 - 现象:汪汪声浪入口如果继续切换到独立配置阶段,会和拼图、抓大鹅的创作页内嵌结构不一致,用户会感觉入口跳页。 @@ -187,7 +293,7 @@ ## 陶泥儿 logo 生图慢请求先缩短 prompt 并单张串行 -- 现象:使用 VectorEngine `gpt-image-2-all` 生成陶泥儿 logo 概念图时,部分 prompt 会超过 10 分钟仍无响应,或返回 `429` / `当前分组上游负载已饱和`;同一批次里后续图片会被前面的慢请求拖住。 +- 现象:使用 VectorEngine `gpt-image-2` 生成陶泥儿 logo 概念图时,部分 prompt 会超过 10 分钟仍无响应,或返回 `429` / `当前分组上游负载已饱和`;同一批次里后续图片会被前面的慢请求拖住。 - 原因:复杂抽象 logo prompt 同时包含品牌解释、禁用元素、中文结构和多重隐喻时,上游排队与生成时长不稳定;并发或批量运行会放大单条慢请求的影响。 - 处理:先 `--dry-run` 看请求体;真实生成时优先短 prompt、单一造型、单张串行或小批量。失败后不要反复重试同一长 prompt,先压缩到“一个主体 + 一个负形 + 颜色 + 禁用文字/播放键/聊天气泡”再跑。联系表中的中文标签不要通过 PowerShell 管道内联 Python 写入,容易因编码链路显示为问号,可改用英文标签或脚本文件方式。 - 验证:生成文件落在 `public/branding/taonier-logo-*/`,用 Pillow 检查图片尺寸和非空;执行 `node --check scripts/generate-taonier-logo-concepts.mjs`、`npm run check:encoding`、`git diff --check`。 @@ -205,11 +311,11 @@ - 现象:点击生成抓大鹅草稿后,页面只提示“服务暂不可用”,或者本地 `npm run dev:api-server` 看似启动但生成接口不可用。 - 原因:配置缺失类错误通常在后端 `error.details.reason` 中给出具体缺项,前端如果只读 `details.message` 会吞掉原因;本地只配置 `ALIYUN_OSS_BUCKET` / `ALIYUN_OSS_ENDPOINT` 时,旧逻辑还会在启动期构造空 AccessKey 的 OSS 客户端并失败。抓大鹅新链路仍是 2D 生图切割,不需要也不应回退 Rodin/GLB。 -- 处理:前端 API 错误展示优先读取 `details.reason`,再读取 `details.message`,避免底层 `error sending request` 覆盖真正可操作的配置或网络原因;`api-server` 只有在 OSS 四件套齐全时初始化 OSS 客户端,部分缺失只记 warning 并让具体 generated 上传/换签接口返回 `OSS 未完成环境变量配置`。抓大鹅素材、封面和背景生成在调用 VectorEngine 前先预检 OSS,并通过 `details.missingEnv` 列出缺项;真实生成需补齐 `VECTOR_ENGINE_BASE_URL`、`VECTOR_ENGINE_API_KEY` 和完整 `ALIYUN_OSS_*` 四件套。抓大鹅 `5*5` 素材图提示词还必须要求相邻物体主体至少保留 `1/4` 单格宽度空白间距,避免切割后相邻格内容污染。 +- 处理:前端 API 错误展示优先读取 `details.reason`,再读取 `details.message`,避免底层 `error sending request` 覆盖真正可操作的配置或网络原因;`api-server` 只有在 OSS 四件套齐全时初始化 OSS 客户端,部分缺失只记 warning 并让具体 generated 上传/换签接口返回 `OSS 未完成环境变量配置`。抓大鹅素材、封面和背景生成在调用 VectorEngine 前先预检 OSS,并通过 `details.missingEnv` 列出缺项;真实生成需补齐 `VECTOR_ENGINE_BASE_URL`、`VECTOR_ENGINE_API_KEY` 和完整 `ALIYUN_OSS_*` 四件套。抓大鹅 UI spritesheet 和物品 spritesheet 的提示词必须要求纯绿色绿幕背景,后端上传 OSS 前统一扣成透明 PNG,避免运行态 alpha 连通域解析失败。 - 验证:`npm run test -- src/services/apiClient.test.ts` 覆盖 `details.reason`;`cargo test -p api-server state --manifest-path server-rs/Cargo.toml` 覆盖半配置 OSS 不阻断启动;`npm run dev:api-server` 后按实际 `GENARRATIVE_API_PORT` 请求 `/healthz`,不要默认打 `3100`。 - 关联:`packages/shared/src/http.ts`、`server-rs/crates/api-server/src/state.rs`、`docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md`、`docs/technical/AUTH_SNAPSHOT_AND_MATCH3D_LOCAL_DEV_FIX_2026-05-01.md`。 -2026-05-14 补充:抓大鹅“物品素材 sheet”已改用 VectorEngine Gemini `gemini-3-pro-image-preview` 原生 `generateContent`,真实生成读取 `VECTOR_ENGINE_BASE_URL`、`VECTOR_ENGINE_API_KEY` 和 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;封面和 `9:16` 背景图走 VectorEngine `/v1/images/generations`,`1:1` 容器 UI 走 VectorEngine `/v1/images/edits` multipart 参考图链路。排查素材 sheet 时看请求路径是否为 `/v1beta/models/gemini-3-pro-image-preview:generateContent?key=...`,响应图片在 `candidates[].content.parts[].inlineData.data` / `inline_data.data`,不要再按 APIMart `/images/generations` 或 `/tasks/{task_id}` 排查。 +2026-05-22 补充:抓大鹅“物品 spritesheet”不再按旧 Gemini `generateContent` / `5*5` sheet 路径排查;当前链路先用 `gpt-image-2` 无参考图生成 `9:16` 关卡整图,再以该关卡整图作为 multipart `image` 参考并发编辑生成 `1K 1:1` UI spritesheet、`1K 9:16` 背景图和 `2K 1:1` 物品 spritesheet。UI 与物品 spritesheet 都要求纯绿色绿幕背景,上传 OSS 前通过后端透明化处理写入真实 alpha PNG。 ## 抓大鹅发布按钮要先开发布面板,封面编辑收口到发布面板内 @@ -303,7 +409,7 @@ ## 儿童动作 Demo 绘本风资源未生成先查 VectorEngine 配置 - 现象:`/child-motion-demo` 已经呈现绘本草地风格,但 `public/child-motion-demo/picture-book-grass-stage.png`、`picture-book-grass-floor.png`、`picture-book-ground-ring.png`、`picture-book-character-outline.png`、`picture-book-ui-panel.png` 或 `picture-book-ui-button.png` 不存在,Network 里对应图片返回 404,或运行 `npm run assets:child-motion-demo -- --live` 返回缺少 VectorEngine 配置。 -- 原因:儿童动作 Demo 的真实背景、地面、UI、地面指示环和角色轮廓资源都使用 VectorEngine `gpt-image-2-all` 生成,脚本只读取 `VECTOR_ENGINE_BASE_URL`、`VECTOR_ENGINE_API_KEY` 和可选 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;仓库内不能提交真实 key,缺配置时页面只能使用 CSS 草地绘本兜底。 +- 原因:儿童动作 Demo 的真实背景、地面、UI、地面指示环和角色轮廓资源都使用 VectorEngine `gpt-image-2` 生成,脚本只读取 `VECTOR_ENGINE_BASE_URL`、`VECTOR_ENGINE_API_KEY` 和可选 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;仓库内不能提交真实 key,缺配置时页面只能使用 CSS 草地绘本兜底。 - 处理:在本地私密环境补齐 `VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai` 与 `VECTOR_ENGINE_API_KEY`,不要把 key 写入 Git;先运行 `npm run assets:child-motion-demo -- --dry-run` 核对 prompt,再运行 `npm run assets:child-motion-demo -- --live` 或 `npm run assets:child-motion-demo -- --live --only ui-panel` 等小批量命令生成资源。透明资源的品红底源图写入 `tmp/child-motion-demo-assets/`,不要把源图或预览图放入 `public/child-motion-demo/` 作为正式资产。 - 验证:生成后确认 `public/child-motion-demo/` 只保留页面引用的最终 PNG,重新打开 `/child-motion-demo` 可看到真实绘本草地背景、地面、圆环、角色轮廓和 UI 资源;`npm run check:encoding` 仍通过。 - 关联:`scripts/generate-child-motion-demo-assets.mjs`、`src/index.css`、`docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md`。 @@ -327,8 +433,8 @@ ## GPT-image-2 不再读 APIMart 图片配置 - 现象:配置了 `APIMART_BASE_URL` / `APIMART_API_KEY` 后,RPG、拼图或方洞的 GPT-image-2 生图仍返回缺配置,或请求体里还出现 `official_fallback` / `image_urls`。 -- 原因:2026-05-09 后 GPT-image-2 图片生成已切到 VectorEngine `gpt-image-2-all`,APIMart 只保留给创意 Agent 的 `gpt-5` Responses 文本/多模态链路。 -- 处理:为图片生成配置 `VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai`、`VECTOR_ENGINE_API_KEY`、`VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;排查请求体时确认路径为 `/v1/images/generations`、模型为 `gpt-image-2-all`、参考图字段为 `image`。 +- 原因:2026-05-21 后 GPT-image-2 图片生成按 VectorEngine 创建/编辑接口分流,APIMart 只保留给创意 Agent 的 `gpt-5` Responses 文本/多模态链路。 +- 处理:为图片生成配置 `VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai`、`VECTOR_ENGINE_API_KEY`、`VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;排查请求体时确认无参考图路径为 `/v1/images/generations`、有参考图路径为 `/v1/images/edits`,模型为 `gpt-image-2`。 - 验证:运行 `cargo test -p api-server openai_image --manifest-path server-rs/Cargo.toml` 和相关玩法图片生成测试;真实联调只在本地私密环境放置 VectorEngine key。 - 关联:`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md`、`server-rs/crates/api-server/src/openai_image_generation.rs`。 @@ -348,19 +454,19 @@ - 验证:后端单测覆盖 `build_puzzle_levels_with_primary_update` 和 `apply_generated_puzzle_candidates_to_session_snapshot`;结果页重新生成应在未重新上传时继续带入 `level.pictureReference`。 - 关联:`server-rs/crates/api-server/src/puzzle.rs`、`src/components/puzzle-result/PuzzleResultView.tsx`。 -## 拼图图生图仍不像参考图时先看是否走了 edits +## 拼图参考图不像时先看 edits multipart image - 现象:Network payload 已带 `referenceImageSrc`,但 VectorEngine 生成结果仍明显不像上传图。 -- 原因:`gpt-image-2-all` 的 `/v1/images/generations` 更适合纯文生图;有参考图且需要重绘时应切到 `/v1/images/edits` 的 multipart 图生图接口。 -- 处理:`referenceImageSrc` 存在且 `aiRedraw = true` 时直接走 edits,prompt 仍保留参考图强约束;入口页关闭 AI 重绘时直接应用上传图,不调用图片生成;前端把参考图压到单边 1024 内,后端解析后拒绝超过 8MB 的参考图字节。 -- 验证:后端单测应覆盖 `images/edits` 路由、`b64_json` 响应解码和参考图强提示;真实联调先看日志里是否命中 `拼图 VectorEngine 图片编辑 HTTP 返回`。 +- 原因:参考图只在 `aiRedraw = true` 时由后端解析并传给 `gpt-image-2` `/v1/images/edits` 的 multipart `image` part;若前端没传 `referenceImageSrc`、后端解析失败或 prompt 缺少参考图强约束,生成会退化为纯文生图。 +- 处理:`referenceImageSrc` 存在且 `aiRedraw = true` 时走 edits multipart,prompt 保留参考图强约束;入口页关闭 AI 重绘时直接应用上传图,不调用图片生成;前端把参考图压到单边 1024 内,后端解析后拒绝超过 8MB 的参考图字节。 +- 验证:后端单测应覆盖 `/v1/images/edits` 路由、`b64_json` 响应解码和参考图强提示;真实联调看日志里是否命中 `拼图 VectorEngine 图片编辑 HTTP 返回`。 - 关联:`server-rs/crates/api-server/src/puzzle.rs`、`src/services/puzzleReferenceImage.ts`、`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md`。 ## 拼图 edits 报 error sending request 先看网络分类 - 现象:拼图有参考图时返回 `拼图图片生成失败:创建拼图 VectorEngine 图片编辑任务失败:error sending request for url (https://api.vectorengine.ai/v1/images/edits)`,后端没有 `拼图 VectorEngine 图片编辑 HTTP 返回` 日志。 - 原因:这是 `reqwest` 在 `send()` 阶段失败,尚未收到 VectorEngine HTTP 响应;常见原因是服务器网络 / DNS / 防火墙 / 代理问题,或上游网关中断 multipart 连接。 -- 处理:查看错误响应 `details.reason/source/connect/body/timeout/endpoint` 和 `拼图 VectorEngine 请求发送失败` 日志。拼图图片客户端已强制 HTTP/1.1,降低 multipart HTTP/2 兼容风险;若 `connect=true` 先查网络出口,若 `body=true` 先查参考图大小和 multipart 发送。 +- 处理:查看错误响应和 `拼图 VectorEngine 图片编辑` 相关日志;若请求发送阶段失败,先查网络出口、DNS、防火墙、代理、参考图大小和 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`。 - 验证:`curl --http1.1 -i -X POST https://api.vectorengine.ai/v1/images/edits -H "Authorization: Bearer invalid" -F "model=gpt-image-2" -F "prompt=test" -F "n=1" -F "size=1024x1024" -F "image=@public/match3d-background-references/pot-fused-reference.png;type=image/png"` 至少应返回 HTTP `401`,说明域名、TLS、路径和 multipart 上传可达;执行 `cargo test -p api-server puzzle_vector_engine --manifest-path server-rs/Cargo.toml`。 - 关联:`server-rs/crates/api-server/src/puzzle.rs`、`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md`、`docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md`。 @@ -391,17 +497,41 @@ ## 拼图草稿生成 180 秒后 502/504 先查 VectorEngine 超时与前端重试 - 现象:点击“生成拼图游戏草稿”后,`POST /api/runtime/puzzle/agent/sessions/{sessionId}/actions` 等待约 180 秒返回 `502 Bad Gateway` 或 `504 Gateway Timeout`;钱包流水里同一 session 可能出现连续两组 `puzzle_initial_image` 扣费后退款。 -- 原因:首图生成走 VectorEngine `gpt-image-2-all`,默认 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=1000000`;若上游在该窗口内未返回,后端退款并返回超时错误。旧前端 action 写请求会对 502/503/504 自动重试一次,导致同一次点击重复触发生图与扣退费。 +- 原因:首图生成走 VectorEngine `gpt-image-2`,默认 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=1000000`;若上游在该窗口内未返回,后端退款并返回超时错误。旧前端 action 写请求会对 502/503/504 自动重试一次,导致同一次点击重复触发生图与扣退费。 - 处理:拼图/创作 Agent 的 `executeAction` 默认不做前端自动重试;后端将 VectorEngine / 图片请求超时映射为 `504 Gateway Timeout`,`error.details.provider=vector-engine` 且 `timeout=true`。真实排障按日志同一 `session_id` 查 `拼图 VectorEngine 图片生成 HTTP 返回` 是否缺失,以及钱包流水扣费到退款的时间差是否接近 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`。 - 验证:运行 `npm run test -- src/services/creation-agent/creationAgentClientFactory.test.ts src/services/apiClient.test.ts`、`cargo test -p api-server puzzle_vector_engine --manifest-path server-rs/Cargo.toml`,真实联调重启 `npm run dev:api-server` 后检查 `/healthz`。 - 关联:`src/services/creation-agent/creationAgentClientFactory.ts`、`server-rs/crates/api-server/src/puzzle.rs`、`docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md`。 +## 开局 CG 故事板生图失败先查 VectorEngine 请求预算和旧进程 + +- 现象:RPG 结果页点击开局 CG 后,`POST /api/runtime/custom-world/opening-cg` 在较长等待后返回“开局 CG 故事板生成失败:创建图片生成任务失败:error sending request for url (https://api.vectorengine.ai/v1/images/generations)”。 +- 原因:该故事板会把角色图和首幕背景图作为参考图一起传给 VectorEngine `gpt-image-2-all`,请求体和上游生成耗时都比普通单图更大;若运行中的 `api-server` 仍沿用旧 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`,或者参考图过大,会在请求发送/等待阶段被 reqwest 截断。日志里 `timeout=false connect=false request=true body=false source=client error (SendRequest)` 表示还没拿到上游 HTTP 响应,通常优先怀疑大 JSON 请求体、上游网关中断或 HTTP 协议兼容,而不是业务响应解析失败。直接请求 VectorEngine 若无效 token 可快速返回 401,不能据此判断真实生图不会超时。 +- 处理:开局 CG 参考图入参先压到单边 768 的 JPEG;`/v1/images/generations` 保持 reqwest 默认 HTTP 协商,只有 multipart `/v1/images/edits` 单独强制 HTTP/1.1。后端图片 helper 将 `request_body_bytes`、每张参考图 Data URL 长度、`timeout/connect/body/source/rootSource/sourceChain/endpoint` 分类写入日志和 `error.details`,前端优先展示 `details.reason`。修改 `.env.secrets.local` 后必须重启 `api-server`,`npm run dev` 终端用 `rs api-server`,否则旧进程仍按旧超时运行。 +- 验证:分别运行 `cargo test -p api-server custom_world_ai --manifest-path server-rs/Cargo.toml` 和 `cargo test -p api-server openai_image_generation --manifest-path server-rs/Cargo.toml`;真实联调重启后再触发开局 CG,若仍失败看返回的 `details.reason/source/rootSource/sourceChain/timeout/connect/body/endpoint` 和 `logs/api-server/` 同一 request_id。 +- 关联:`server-rs/crates/api-server/src/custom_world_ai.rs`、`server-rs/crates/api-server/src/custom_world_ai/opening_cg.rs`、`server-rs/crates/api-server/src/openai_image_generation.rs`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 + +## 开局 CG 成功后又变空白要保留 profile.openingCg + +- 现象:RPG 结果页里的开局 CG 成功显示一瞬后,窗口又退回空白占位。 +- 原因:`openingCg` 只存在于结果页 profile 槽位,如果父层在 `onProfileChange` 后重新同步了 profile,却经过 `normalizeCustomWorldProfileRecord` 或作品库写回时丢掉 `openingCg`,预览就会从视频 / 故事板回退为空白。 +- 处理:`src/data/customWorldLibrary.ts` 的 profile 归一化必须透传 `openingCg`;结果页和父层后续同步都应把它当作受控资产槽位,而不是临时 UI 状态。 +- 验证:`npm run test -- src/data/customWorldLibrary.test.ts src/components/CustomWorldResultView.test.tsx`,确认生成后即使父层做一次归一化回写,开局 CG 仍继续显示。 +- 关联:`src/data/customWorldLibrary.ts`、`src/components/rpg-creation-result/RpgCreationResultViewImpl.tsx`、`src/components/CustomWorldEntityCatalog.tsx`。 + +## RPG 发布报 legacy_result_profile_json 非法先查 null 兼容 + +- 现象:RPG 结果页发布动作返回 `UPSTREAM_ERROR`,SpacetimeDB details 里是 `custom_world.compile.legacy_result_profile_json 不是合法 JSON object`。 +- 原因:`publish_world` 前端契约只要求 `{ action: 'publish_world' }`;`ExecuteCustomWorldAgentActionRequest.legacy_result_profile` 是可选字段,经 HTTP / serde / SpacetimeDB payload 传递时可能显式成为 JSON `null`。旧的编译器只接受 object 或缺省,把 `Some("null")` 当成非法 legacy JSON。 +- 处理:`module-custom-world` 的 optional JSON object 解析要把 `null` 视为未提供,仍拒绝数组、字符串、数字和坏 JSON;正式发布继续以 session `draft_profile_json` 为草稿真相。 +- 验证:`cargo test -p module-custom-world published_profile_compile --manifest-path server-rs/Cargo.toml`。 +- 关联:`server-rs/crates/module-custom-world/src/application.rs`、`server-rs/crates/spacetime-module/src/custom_world.rs`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + ## 本地脚本调 VectorEngine 生图卡住先区分 fetch 首部超时 - 现象:用 Node `fetch` 直接请求 `POST /v1/images/generations`,已经设置较长的 AbortController 超时,但仍在约 180 到 300 秒后抛 `AbortError`、`TypeError: fetch failed` 或 `UND_ERR_HEADERS_TIMEOUT`;同一 prompt 改用原生 `https.request` 可以在较短时间内成功返回图片。 - 原因:Node/Undici 的默认 headers timeout 可能早于业务脚本期望的长生图等待窗口触发,表现上容易被误判成 VectorEngine 上游本身超时。 - 处理:长期脚本优先复用后端 reqwest 或项目已有生成脚本;临时本地工具若必须用 Node,可改用原生 `http`/`https.request` 并显式设置 socket timeout,或为 Undici 单独配置 headers timeout。仍需隐藏 `VECTOR_ENGINE_API_KEY`,只报告配置是否存在。 -- 验证:同一 `gpt-image-2-all` 请求体、同一环境变量下,原生 HTTP 请求能返回 `url` / `b64_json` 并落盘;失败时错误里能区分请求发送、首部等待、下载和解码阶段。 +- 验证:同一 `gpt-image-2` 请求体、同一环境变量下,原生 HTTP 请求能返回 `url` / `b64_json` 并落盘;失败时错误里能区分请求发送、首部等待、下载和解码阶段。 - 关联:`.codex/skills/gpt-image-2-apimart/SKILL.md`、`server-rs/crates/api-server/src/openai_image_generation.rs`。 ## 旧后端路线文档造成判断漂移 @@ -774,6 +904,14 @@ - 验证:运行 `git ls-files --stage scripts/prepare-server-provision-tools.sh`,确认 mode 为 `100755`;重新跑 `Genarrative-Server-Provision` 时应进入工具下载/打包日志,而不是停在 `Permission denied`。 - 关联:`jenkins/Jenkinsfile.production-server-provision`、`scripts/prepare-server-provision-tools.sh`、`scripts/jenkins-checkout-source.sh`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 +## Server-Provision 下载阶段不要放回 genarrative-build-01 + +- 现象:`Genarrative-Server-Provision` 日志里 `Prepare Provision Tools` 显示 `Running on genarrative-build-01 in /root/...`,随后在该节点上下载 GitHub release 或 `install.spacetimedb.com` 失败。 +- 原因:`genarrative-build-01` 在当前 provision 流程里是 Linux 目标发布机/目标 agent,不是用户本地 Windows 下载环境;把下载阶段放在 `linux && genarrative-build` 等于让目标机自己外连。 +- 处理:下载必须发生在 Jenkins `windows` 节点的 `Download Provision Tool Archives` 阶段,先下载 SpacetimeDB Linux release tarball 和 `otelcol-contrib` Linux amd64 包,再 `stash/unstash` 到目标 Linux 节点。目标机执行 `scripts/prepare-server-provision-tools.sh` 时设置 `PROVISION_REQUIRE_LOCAL_DOWNLOADS=true`,缺少下载件直接失败,不回退联网下载。 +- 验证:Jenkins 日志应先出现 `Running on ... windows` 和 `[prepare-provision-downloads] 下载 ...`,目标节点只出现 `[prepare-provision-tools] 使用已下载的 ...`;如果目标节点出现 `下载 otelcol-contrib:` 或 `下载 SpacetimeDB 官方安装器脚本:`,说明又回退到错误路径。 +- 关联:`jenkins/Jenkinsfile.production-server-provision`、`scripts/prepare-server-provision-tools.sh`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 + ## 个人任务 scope 不得扩成 work/site/module - 现象:个人任务配置为 `work` / `site` / `module` 后进度串桶或静默按 0 处理。 @@ -810,7 +948,7 @@ - 现象:修改抓大鹅素材时容易沿用旧 Rodin/GLB 方案,导致新草稿生成耗时变长、进度停在模型阶段,或运行态等待不存在的 GLB。 - 原因:仓库里保留了 Hyper3D 通用代理和历史模型字段,旧文档也曾要求草稿阶段同步生成 GLB。当前产品口径已经改为 2D 多视角素材。 -- 处理:新 `match3d_compile_draft` 与批量新增只生成 2D 图片:每个物品 5 个视角,单张 1K 素材图固定 5x5,最多承载 5 个物品,一行对应一个物品,不足 5 个物品也补齐到完整 5 行;超过 5 个物品自动分批并行生图。素材图 prompt 固定要求纯绿色绿幕背景,切割前先把绿幕处理为透明 alpha,再做格内内容前景边界校准并带留白,避免固定内缩切掉贴近格线的主体。`generatedItemAssets[].status` 使用 `image_ready`,发布校验看 `imageViews[]` 或首图引用。`generated-models` 仅用于历史外部模型链接转存,不能作为新生产链路。 +- 处理:新 `match3d_compile_draft` 与批量新增只生成 2D 图片:每个物品 5 个形态,单张 `2K 1:1` 物品 spritesheet 固定 `10*10`,每行承载两种物品、每种五个形态,单张最多承载 20 种物品。素材图 prompt 固定要求纯绿色绿幕背景,上传 OSS 前先把整张 spritesheet 绿幕处理为透明 alpha,再由运行态和编辑器按 alpha 连通域解析;`generatedItemAssets[].status` 使用 `image_ready`,发布校验看 `imageViews[]`、首图引用或可解析的物品 spritesheet。`generated-models` 仅用于历史外部模型链接转存,不能作为新生产链路。 - 验证:`cargo test -p api-server match3d --manifest-path server-rs/Cargo.toml`、`npm run test -- src\services\miniGameDraftGenerationProgress.test.ts src\components\match3d-result\Match3DResultView.test.tsx src\components\match3d-runtime\Match3DRuntimeShell.test.tsx`。 - 关联:`server-rs/crates/api-server/src/match3d.rs`、`src/components/match3d-runtime/Match3DRuntimeShell.tsx`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。 @@ -846,11 +984,11 @@ - 验证:执行 `npm run test -- src/components/match3d-runtime/Match3DRuntimeShell.test.tsx` 和 `npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "Match3D runtime"`;浏览器 Network 中背景和容器 generated path 应先请求 `/api/assets/read-url` 换签,局内出现 `match3d-background-image` 和 `match3d-container-image` 对应图片。 - 关联:`src/components/match3d-runtime/Match3DRuntimeShell.tsx`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`src/components/rpg-entry/rpgEntryWorldPresentation.ts`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。 -## 抓大鹅容器参考图必须走 edits 并接管棋盘外观 +## 抓大鹅容器参考图必须进入 edits multipart image 并接管棋盘外观 - 现象:抓大鹅结果页看似有容器生成入口,但真实生成出的局内容器不像 `pot-fused-reference.png`,或进入试玩后仍被默认圆形锅壳、金色边框和径向底色覆盖/裁切。 -- 原因:`/v1/images/generations` 的 `image` 数组更适合弱参考文生图,难以稳定锁定大尺寸轻俯视容器构图;即使生成了容器图,如果运行态继续保留默认 `rounded-full` 锅壳和 `overflow-hidden`,生成图也会被默认视觉覆盖或裁掉。 -- 处理:抓大鹅 `1:1` 容器 UI 图必须用 VectorEngine `POST /v1/images/edits` multipart,把 `public/match3d-background-references/pot-fused-reference.png` 作为 `image` part 上传;共享 GPT-image-2 HTTP client 承载 multipart 时强制 HTTP/1.1。`Match3DRuntimeShell` 在容器图换签并成功加载后,把棋盘外壳切为透明和 `overflow-visible`,只在容器缺失或加载失败时使用默认圆形容器。 +- 原因:容器参考图必须进入 `gpt-image-2` `/v1/images/edits` multipart `image` part,并配合强 prompt 锁定大尺寸轻俯视容器构图;即使生成了容器图,如果运行态继续保留默认 `rounded-full` 锅壳和 `overflow-hidden`,生成图也会被默认视觉覆盖或裁掉。 +- 处理:抓大鹅 `1:1` 容器 UI 图统一调用 VectorEngine `POST /v1/images/edits`,参考 `public/match3d-background-references/pot-fused-reference.png` 的透明容器图由后端作为 `image` part 上传;该参考图属于后端生图协议输入,需通过 `include_bytes!` 编译进 `api-server`,不能在运行时按当前工作目录读取 `public/`。`Match3DRuntimeShell` 在容器图换签并成功加载后,把棋盘外壳切为透明和 `overflow-visible`,只在容器缺失或加载失败时使用默认圆形容器。 - 验证:执行 `cargo test -p api-server vector_engine --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server match3d_background --manifest-path server-rs/Cargo.toml`、`npm run test -- src/components/match3d-runtime/Match3DRuntimeShell.test.tsx src/components/match3d-result/Match3DResultView.test.tsx`;真实联调看容器生成请求是否命中 `/v1/images/edits`,局内 `match3d-container-image` 是否渲染且 `match3d-board` 不再含默认 `rounded-full`。 - 关联:`server-rs/crates/api-server/src/openai_image_generation.rs`、`server-rs/crates/api-server/src/match3d.rs`、`src/components/match3d-runtime/Match3DRuntimeShell.tsx`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。 @@ -928,9 +1066,9 @@ ## 抓大鹅难度配置的物品种类和消除次数必须分离 -- 现象:历史草稿选择标准 / 硬核难度后,系统可能把 `clearCount` 当成局内物品种类数量,导致标准需要 12 种、硬核需要 20/21 种;素材不足时发布或试玩行为不一致。 +- 现象:历史草稿选择标准 / 硬核难度后,系统可能把 `clearCount` 当成局内物品种类数量,导致标准需要 12 种、硬核需要 20/21 种;或者把第 11 到 20 个物品持久化为第 11 到 20 行,触发“系列素材图集持久化的行列索引必须落在 n*n 范围内”。 - 原因:旧运行态把消除次数和类型数量绑在一起,结果页文案又同时展示“素材图片 / 局内类型”,导致前端、发布校验和 run start 口径不一致。 -- 处理:统一使用 `物品种类` 口径:轻松 3、标准 9、进阶 15、硬核 21;历史 `clearCount=20` 且难度为硬核的运行态按新硬核升为 21 组三消,避免 20 组却要求 21 种素材。发布前按 `image_ready` 且有 `imageViews[]` 或 `imageSrc/imageObjectKey` 的生成素材数量阻断不足难度;试玩不阻断,但通过 `itemTypeCountOverride` 自动降到已生成 2D 素材数量。重启从已有 run 快照反推实际物品种类,保持同一局重开不变。 +- 处理:生成和持久化固定使用 20 个物品素材;运行态物品种类口径为轻松 3、标准 9、进阶 15、硬核 20,历史 `clearCount=20` 且难度为硬核的运行态仍可升为 21 组三消,但类型池不超过 20。10*10 sheet 每行两种物品、每种五个形态,持久化行列为 `row = itemIndex / 2 + 1`、`col = itemIndex % 2 * 5 + viewIndex + 1`。发布前按 `image_ready` 且有 `imageViews[]` 或 `imageSrc/imageObjectKey` 的生成素材数量阻断不足难度;试玩不阻断,但通过 `itemTypeCountOverride` 自动降到已生成 2D 素材数量。重启从已有 run 快照反推实际物品种类,保持同一局重开不变。 - 验证:`npm run test -- src\components\match3d-result\Match3DResultView.test.tsx`、`cargo test -p module-match3d --manifest-path server-rs\Cargo.toml`,涉及发布 reducer 时补跑 `cargo test -p spacetime-module match3d --manifest-path server-rs\Cargo.toml`。 - 关联:`src/components/match3d-result/Match3DResultView.tsx`、`src/services/match3d-runtime/match3dRuntimeClient.ts`、`server-rs/crates/module-match3d/src/application.rs`、`server-rs/crates/spacetime-module/src/match3d.rs`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。 @@ -947,7 +1085,7 @@ - 现象:抓大鹅生成的物品视角图裁剪后仍带白边,或者整块纯绿色绿幕背景没有被透明化,运行态看到绿色方块。 - 原因:素材 sheet 可能是“每格内部绿幕、整张图外圈近白底”,内部绿幕不一定连通到 sheet 外边缘;旧 flood fill 只从外边缘找背景会漏掉这种绿幕块。白底抗锯齿如果不纳入抠像和边缘去污染,也会随裁剪输出成一圈白边。即使顺序已是先整张 sheet 去绿再裁剪,较厚的半透明或混色软绿边仍可能低于高置信绿幕阈值,被当作前景带进独立 PNG。 - 处理:`api-server` 的 `slice_match3d_material_sheet` 必须先在整张 sheet 上做透明背景后处理:外边缘连通绿幕/近白底清 alpha,非连通但高置信纯绿块也清 alpha,沿整张 sheet 透明背景继续吃掉软绿边,边缘近白和绿幕抗锯齿做透明或去污染;同时保护不够纯的绿色主体像素。不要改成先裁剪单格再去绿。 -- 验证:`cargo test -p api-server match3d_material_sheet_slicing --manifest-path server-rs\Cargo.toml` 覆盖非连通绿幕、白边、贴边主体保留和固定 5x5 切图。 +- 验证:`cargo test -p api-server match3d_material_sheet_slicing --manifest-path server-rs\Cargo.toml` 覆盖非连通绿幕、白边、贴边主体保留和固定 `10*10` 切图;`cargo test -p api-server match3d_spritesheet_green_screen_postprocess_turns_background_transparent --manifest-path server-rs\Cargo.toml` 覆盖完整 spritesheet 上传前绿幕透明化。 - 关联:`server-rs/crates/api-server/src/match3d.rs`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。 ## 抓大鹅物品详情大方格只做单张大图查看 @@ -1014,6 +1152,14 @@ - 验证:运行 `npm run test -- src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx -t "拖拽合并大块时底层单格不显示选中色块"`,并确认合并块拖拽时底层 `[data-piece-id]` 仍为 `puzzle-runtime-piece--merged`。 - 关联:`src/components/puzzle-runtime/PuzzleRuntimeShell.tsx`、`src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx`、`docs/technical/PUZZLE_FORM_CREATION_FLOW_2026-04-29.md`。 +## 推荐页嵌入拼图通关结算不要放在运行态内部 absolute 层 + +- 现象:推荐页里玩拼图通关后,结算面板只显示上半部分,排行榜、下一关按钮或相似作品卡被截断。 +- 原因:推荐页把运行态放在滑动作品卡的视觉区内,`platform-recommend-swipe-page`、`platform-recommend-swipe-card__visual` 和 `platform-recommend-runtime-viewport` 都是 `overflow: hidden`;拼图通关结算如果仍是运行态内部 `absolute inset-0` 弹层,就只能在半屏卡片区域里显示。 +- 处理:`PuzzleRuntimeShell` 在 `embedded` 模式下把通关结算层通过 portal 挂到 `document.body`,使用 `puzzle-runtime-modal-overlay--fixed` 页面级 fixed 浮层;非嵌入态继续使用运行态内部覆盖层。 +- 验证:运行 `npm run test -- src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx -t "推荐页嵌入拼图通关结算使用页面级浮层避免卡片裁剪"`,确认弹层不再位于 `.platform-recommend-runtime-viewport` 内。 +- 关联:`src/components/puzzle-runtime/PuzzleRuntimeShell.tsx`、`src/index.css`、`src/components/rpg-entry/RpgEntryHomeView.tsx`。 + ## 拼图历史图片列表不要把账号归属当图片名 - 现象:拼图创作页或结果页打开“选择历史图片”后,历史列表显示 `账号 user-1` 之类归属文案而不是图片名;`1713686400.000000Z` 这类时间显示为未知;选中后预览或生成参考图可能被怀疑不可用。 @@ -1032,12 +1178,22 @@ ## 拼图结果页局部生图不要污染草稿生成态 -- 现象:拼图草稿已经生成完成后,在结果页重新生成 UI 背景或追加关卡生成图片,草稿页仍显示整卡“生成中”,点击草稿会回到生成过程页,无法查看已有结果;UI 背景生成中还会禁用“新增关卡”和关卡图生成。 +- 现象:拼图草稿已经生成完成后,在结果页重新生成关卡图片或追加关卡生成图片,草稿页仍显示整卡“生成中”,点击草稿会回到生成过程页,无法查看已有结果;关卡图片生成中还会禁用“新增关卡”和其它关卡详情编辑。 - 原因:结果页局部 action 复用了全局 `isPuzzleBusy` / 持久化 `generationStatus=generating` 语义,作品架没有区分“初始草稿不可查看”和“已有结果上的局部关卡生成”。 -- 处理:作品架只在拼图没有可用封面、首关候选图或任一可查看关卡时才把 `generationStatus=generating` 解释为初始草稿生成;结果页 UI 背景和关卡图走 background action,不设置全局 busy,UI 背景只禁用自己的按钮;SpacetimeDB/API mapper 读写时把已有图片但状态仍是 `generating` 的历史关卡归一为 `ready`。 +- 处理:作品架只在拼图没有可用封面、首关候选图或任一可查看关卡时才把 `generationStatus=generating` 解释为初始草稿生成;结果页关卡图走 background action,不设置全局 busy,只标记对应关卡局部生成进度;SpacetimeDB/API mapper 读写时把已有图片但状态仍是 `generating` 的历史关卡归一为 `ready`。 - 验证:`npm run test -- src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx`、`cargo test -p api-server puzzle --manifest-path server-rs\Cargo.toml`。 - 关联:`src/components/custom-world-home/creationWorkShelf.ts`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`src/components/puzzle-result/PuzzleResultView.tsx`、`server-rs/crates/api-server/src/puzzle/mappers.rs`、`server-rs/crates/spacetime-module/src/puzzle.rs`。 +2026-05-22 补充:结果页关卡详情的“关卡测试”不能把单关 `draft` 传给父级再调用 `updatePuzzleWork`。`updatePuzzleWork` 会同步 `puzzle_work_profile.levels_json` 和 source session 草稿,单关快照会把整份多关卡草稿覆盖成一个关卡,退出重进后只剩最后测试的关卡且序号表现为第一关。修复口径是 `PuzzleResultView` 始终传完整 `syncedDraft`,额外用 `{ levelId }` 指定起始关卡;父级持久化完整 levels 后调用 `startLocalPuzzleRun(item, levelId)`。 + +## 拼图上传图关闭 AI 重绘不要走首图生图 + +- 现象:用户在拼图入口页或结果页关卡详情上传图片并关闭 AI 重绘后,生成页仍显示“生成拼图首图”,或者后端仍调用 `generate_puzzle_image_candidates` 生成第一张 1:1 候选图。 +- 原因:上传图直用路径应把 Data URL 或 `/generated-*` 历史图解析后持久化为 `sourceType=uploaded` 的正式候选,再继续生成 9:16 关卡画面、UI spritesheet 和纯背景;如果只把 `aiRedraw=false` 当作“不参考图片生成”,就会误走首图生成。 +- 处理:入口页用 payload 的 `aiRedraw` 写入生成页 metadata,`puzzleAiRedraw=false` 时进度跳过 `生成拼图首图`;后端 `compile_puzzle_draft` 和结果页 `generate_puzzle_images` 都在 `aiRedraw=false && referenceImageSrc 非空` 时走上传图直用候选。结果页关卡详情必须复用 `CreativeImageInputPanel`,不要把正式图当成可重绘参考图;本次上传或历史选择的图才显示 AI 重绘开关并可删除。 +- 验证:`npm run test -- src/services/miniGameDraftGenerationProgress.test.ts src/components/puzzle-result/PuzzleResultView.test.tsx`、`cargo test -p api-server puzzle_result_level_direct_upload_skips_cover_image_generation --manifest-path server-rs\Cargo.toml`。 +- 关联:`src/services/miniGameDraftGenerationProgress.ts`、`src/components/puzzle-agent/PuzzleAgentWorkspace.tsx`、`src/components/puzzle-result/PuzzleResultView.tsx`、`server-rs/crates/api-server/src/puzzle/draft.rs`、`server-rs/crates/api-server/src/puzzle/generation.rs`。 + ## Jenkins 数据库导入导出脚本先补 Node 工具链 PATH - 现象:`Genarrative-Database-Import` 或 `Genarrative-Database-Export` 运行到迁移脚本时,`bash` 报 `node: command not found`,常见在日志里表现为某个 `sh` 块内第 61 行直接调用 `node` 失败。 @@ -1054,6 +1210,22 @@ - 验证:检查 Jenkins build log 中是否出现 `[jenkins-powershell] user:` 和 `[jenkins-powershell] exe:`,以及 `[stdb-checkout] current HEAD:`。上游 Full Build 传下来的 `COMMIT_HASH` 若已等于当前 GitSCM checkout,日志应显示 `requested commit already matches Jenkins GitSCM checkout` 并继续进入构建阶段;同时确认 `builds//log` 不再停在 `PipelineNodeTreeScanner... Cannot run program "powershell"` 或 Checkout 内部 exit code 5。 - 关联:`jenkins/Jenkinsfile.production-stdb-module-build`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 +## Server-Provision Windows 下载 helper 不要原地重写临时 ps1 + +- 现象:`Genarrative-Server-Provision` 的 Windows 下载阶段已经打印了 `[jenkins-powershell] user:` 和 `[jenkins-powershell] exe:`,但在 `.ps1` 原地 BOM 重写前后仍然返回 `exit code 5` / `拒绝访问`,且下载目录还没创建。 +- 原因:Jenkins `writeFile` 生成的临时 `.ps1` 正被同一个 workspace 里的 PowerShell 进程马上重写成 BOM 文件,这个原地改写在本地 Windows Jenkins 环境里比直接脚本执行更容易碰到 workspace 占用或 ACL 拒绝。对这条流水线来说,BOM 不是必须的执行条件。 +- 处理:`runWindowsPowerShell(...)` 改成先 `writeFile`,再由显式 `powershell.exe` 读取脚本文本并用 `ScriptBlock::Create(...)` 直接在内存中执行,不再对同一个 `.ps1` 做 BOM 重写。Windows 下载脚本里先把 `PROVISION_DOWNLOADS_DIR` 归一到 workspace 绝对路径,并补 `Windows workspace` / `download dir` / `已创建下载目录` 三段日志,方便区分是路径问题还是下载问题。 +- 验证:Jenkins log 应先出现 `[jenkins-powershell] workspace:`、`[jenkins-powershell] loaded bytes:`,再出现 `[prepare-provision-downloads] Windows workspace:` 和 `[prepare-provision-downloads] 已创建下载目录:`;如果下载 URL 故意指到不可达地址,应该只在 `curl 下载失败` 处结束,而不是卡在 BOM 重写前。 +- 关联:`jenkins/Jenkinsfile.production-server-provision`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 + +## SpacetimeDB update installer 不要按带 host 后缀的下载文件名执行 + +- 现象:Server-Provision 目标机阶段已经显示“使用已下载的 SpacetimeDB Linux update installer”,随后报 `Error: unexpected argument '-y' found` 或前置 `unknown command name for spacetimedb-update multicall binary`。 +- 原因:`spacetimedb-update-*` 不是当前离线交付的最终形态,GitHub release 页面真正可比较的缓存对象是 `spacetime-x86_64-unknown-linux-gnu.tar.gz` 这种 release tarball;GitHub release asset API 暴露的是 `digest` / SHA256,不是 MD5。 +- 处理:Windows 下载阶段应直接缓存 release tarball 和 `otelcol-contrib_0.151.0_linux_amd64.tar.gz`,目标机 `scripts/prepare-server-provision-tools.sh` 只解压本地 tarball 生成 `bin/current/spacetimedb-cli` 与 `bin/current/spacetimedb-standalone`,不要再把 update installer 当成最终离线包执行。 +- 验证:Jenkins 目标机日志不再出现 `unexpected argument '-y'`、`unknown command name for spacetimedb-update multicall binary`,后续应继续检查 `bin/current/spacetimedb-cli` 和 `bin/current/spacetimedb-standalone` 是否生成。 +- 关联:`scripts/prepare-server-provision-tools.sh`、`jenkins/Jenkinsfile.production-server-provision`。 + ## QQ 浏览器发现页推荐封面全不显示先查 aspect-ratio 兜底 - 现象:发现页的“推荐”子频道作品卡标题、作者和数据正常,但所有封面图不显示,常见于 QQ 浏览器 / X5 等旧移动内核。 diff --git a/.hermes/skills/genarrative-play-type-integration/SKILL.md b/.hermes/skills/genarrative-play-type-integration/SKILL.md index a5b3ee45..61256e7b 100644 --- a/.hermes/skills/genarrative-play-type-integration/SKILL.md +++ b/.hermes/skills/genarrative-play-type-integration/SKILL.md @@ -1,348 +1,253 @@ --- name: genarrative-play-type-integration -description: 在 Genarrative 中新增一个创作入口/玩法类型时,按入口配置、前端分流、契约、后端接口、工作台、结果页、可选 runtime 与作品架的顺序接入。 -version: 1.0.0 -author: Hermes Agent -license: MIT -metadata: - hermes: - tags: [Genarrative, 玩法接入, 创作入口, 前端, 后端, contracts, runtime] - related_skills: [] +description: 在 Genarrative 新增、开放或重构玩法创作工具时,按平台级强约束 SOP 接入入口配置、表单/图片输入创作工作台、单图资产槽位、系列素材图集生成、独立契约、后端 DDD、结果页、运行态、作品架、广场与验证;用于避免复制既有玩法、默认对话式 Agent、页面内手写图片输入或复用玩法专属素材模型。 --- -# Genarrative 新增玩法类型接入流程 +# Genarrative 新增玩法创作工具平台 SOP -用于在 Genarrative 中新增一个创作入口/玩法类型,而不是单纯说明用户如何从入口创建作品。 +把新增玩法当成平台能力接入,不把任何既有玩法当作默认模板。先确定通用模式和契约,再写具体玩法代码。 -## 适用场景 +## 硬性禁区 -- 新增一个游戏玩法入口 -- 让某个玩法从“敬请期待”变为可创建 -- 为新玩法补齐创作工作台、结果页、发布与试玩链路 -- 将新玩法接入创作中心作品架与广场 +- 不恢复前端硬编码入口配置;创作入口事实源必须来自 SpacetimeDB 和 `/api/creation-entry/config`。 +- 不把聊天输入区、流式消息或轻输入 Agent 作为新增玩法默认工作台。 +- 不在新页面内手写图片上传、参考图、AI 重绘、历史图选择、预览或删除确认逻辑。 +- 不把通用系列素材建模成任一玩法专属 DTO;玩法只能追加自己的运行态字段。 +- 不让前端承接正式业务真相;发布、试玩、通关、失败、计分、资产持久化和作品状态以后端投影为准。 +- 不新建平行入口系统、平行作品架或平行公开列表;优先扩展现有平台壳、现有阶段和现有聚合。 +- 不在 UI 面板内默认写功能说明、规则说明或开发解释文案。 -## 先判断接入级别 +## 接入前输入 -### 1. 只做入口占位 +开始编码前,PRD 或当前玩法文档必须已经明确: -只需要新增入口配置,不接 session/workspace/result/runtime。 +- `playId`、对外名称、工程域名、入口 `visible/open` 状态。 +- 创作链路:入口 -> 工作台 -> 生成页 -> 结果页 -> 试玩 -> 发布 -> 运行态。 +- 表单字段:字段名、默认值、校验、后端落库位置、生成提示词来源。 +- 单图资产槽位:`slotId`、`slotType`、`slotName`、提示词来源、读取字段、写回字段、是否允许历史图和 AI 重绘。 +- 系列素材槽位:`batchId` 语义、`sheetSpec`、`slotSpecs`、切图规则、透明化规则、失败回写、局部重生成策略。 +- API 命名空间:`/api/creation//sessions`、`actions`、`works`、`runtime`。 +- 草稿恢复、生成中恢复、失败重试、登录切换、发布后回读和移动端行为。 +- 验证命令和例外声明;没有例外时写明“无创作工具模式例外”。 -适合: -- 敬请期待 -- 灰度占位 +## 默认模式 -### 2. 可进入创作工作台 +新增玩法默认采用表单/图片输入创作工作台: -需要补齐前端分流、session、工作台、结果页,至少能生成草稿。 +```text +创作入口 -> 表单/图片输入工作台 -> 生成页 -> 结果页 -> 试玩 -> 发布 -> 运行态 +``` -### 3. 完整玩法闭环 +工作台只提交结构化表单、图片槽位和配置 payload。确需自然语言对话时,先走“例外流程”,不能把聊天区直接加进默认工作台。 -需要补齐: -- 创作入口 -- 工作台 -- 草稿生成 -- 结果页 -- 发布 -- 试玩 runtime -- 作品架 / 广场 / 分享 +## SOP -## 推荐接入顺序 +### 1. 文档和领域词先行 -### Step 1: 先定玩法 ID 和能力边界 +先读: -先明确: -- `id` 是什么 -- 入口是否可见 -- 是否可点击创建 -- 是否需要对话式创作 -- 是否需要生成中页面 -- 是否需要 result/runtime/gallery/share +- `AGENTS.md` +- `.hermes/shared-memory/` +- `CONTEXT.md` +- `docs/README.md` +- `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md` +- 相关玩法 PRD 或设计文档 -不要先随便起临时 ID 再改名。 +如果文档不能精确指导字段、契约、资产槽位、生成流程和恢复语义,先补文档再编码。新增长期约定时同步 `.hermes/shared-memory/`。 -### Step 2: 新增入口配置 +### 2. 定玩法边界 -文件: -- `src/config/newWorkEntryConfig.ts` +固定 `playId`、对外名称、工程域、入口状态、是否支持结果页、试玩、发布、作品架、广场、分享和 runtime。不要先用临时 ID 接线后再批量改名。 -在 `NEW_WORK_ENTRY_CONFIG.creationTypes` 中新增或调整: -- `id` -- `title` -- `subtitle` -- `badge` -- `visible` -- `open` +### 3. 接入口配置 -字段语义: -- `visible: true`:在创作页签 / 新建作品入口中展示。 -- `visible: false`:不在平台入口展示,但不删除既有玩法路由和能力。 -- `open: true`:可点击进入创作流程。 -- `open: false`:展示为锁定 / 敬请期待,不应进入创建流程。 +入口配置事实源是 SpacetimeDB `creation_entry_type_config`。后台通过 `/admin/api/creation-entry/config` 管理,前台通过 `/api/creation-entry/config` 读取。 -如果只是占位: -- `visible: true` -- `open: false` +前端只允许在展示层派生: -相关渲染与过滤位置: -- `src/components/platform-entry/platformEntryCreationTypes.ts`:将 `NEW_WORK_ENTRY_CONFIG.creationTypes` 映射为平台入口卡片,`getVisiblePlatformCreationTypes()` 会过滤隐藏项,并把可创建模板排在敬请期待模板前面。 -- `src/components/custom-world-home/CustomWorldCreationStartCard.tsx`:创作页签首屏模板入口卡片的实际渲染位置。 -- `src/components/platform-entry/PlatformEntryCreationTypeModal.tsx`:选择创作类型弹层的渲染位置。 +- 可见入口卡片。 +- 锁定或开放状态。 +- 排序、图标、短标题等展示信息。 -注意:当前项目工作区通常已经是 ``,路径不要再额外拼接 `./Genarrative/`。 +`api-server` 路由熔断必须使用同一份入口配置。禁止新增或恢复前端本地默认入口配置作为事实源。 -### Step 3: 确认类型过滤逻辑 +### 4. 前端阶段 -文件: -- `./Genarrative/src/components/platform-entry/platformEntryCreationTypes.ts` +按需要扩展 `SelectionStage`: -检查: -- `getVisiblePlatformCreationTypes()` 是否能展示新类型 -- `isPlatformCreationTypeVisible()` 是否能识别新类型 -- `locked` / `hidden` 是否正确映射 +- `-workspace` +- `-generating` +- `-result` +- `-runtime` +- `-gallery-detail` -### Step 4: 扩展页面阶段 +阶段名可以按玩法命名,UI 形态必须仍是表单/图片创作工作台。进入工作台时只初始化结构化草稿状态,不启动默认聊天会话。 -文件: -- `./Genarrative/src/components/platform-entry/platformEntryTypes.ts` +### 5. 工作台实现 -为新玩法补充 `SelectionStage`: -- `*-agent-workspace` -- `*-generating`(可选) -- `*-result` -- `*-runtime`(可选) -- `*-gallery-detail`(可选) +工作台必须满足: -### Step 5: 在总流程中加类型分流 +- 使用表单控件、图片槽位、风格选项、难度选项、开关和提交按钮组织输入。 +- 单图槽位统一使用 `CreativeImageInputPanel`。 +- 组件缺少能力时先扩展 `CreativeImageInputPanel` 的受控 props,不在玩法页面复制上传、参考图、AI 重绘、历史图、预览或删除确认。 +- 主图读取、裁剪、历史素材弹层、计费确认、自动保存和后端请求由外层页面持有;通用面板只表达输入 UI 和短生命周期 UI 状态。 +- 提交 payload 必须是表单字段与图片槽位结构,不是用户消息文本。 -文件: -- `./Genarrative/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx` +### 6. 单图资产槽位 -在 `handleCreationHubCreateType(type)` 中新增分支,确保: -- 能进入对应工作台 -- 能设置对应 `selectionStage` -- 能关闭类型弹层 +角色形象、UI 背景、容器、封面、分享图、图标等单张图都按单图资产槽位处理。 -同时补: -- `openAgentWorkspace()` -- `leaveFlow()` -- `submitMessage()`(对话式玩法) -- `executeAction()` +统一约定: -### Step 6: 接入通用 Agent flow controller +- 槽位用 `slotId` 稳定标识,`slotType` 表达用途,`slotName` 用于 UI 标签。 +- 上传图、参考图、AI 重绘、历史图选择和删除确认都通过 `CreativeImageInputPanel` 入口表达。 +- 后端写回 `imageSrc`、`imageObjectKey`、`assetObjectId` 中可用字段;前端展示前通过平台资产读取能力换签。 +- 单个槽位重生成只禁用该槽位动作,不阻塞结果页其它槽位、系列素材槽位或导航。 -文件: -- `./Genarrative/src/components/platform-entry/usePlatformCreationAgentFlowController.ts` +### 7. 系列素材图集生成 -如果是 Agent 型玩法,复用通用控制器: -- `createSession` -- `getSession` -- `streamMessage` -- `executeAction` -- `isBusy` +地块、物品、障碍、装饰、UI 部件等一组同类素材都走通用系列素材图集生成流程: + +```text +批量规划 -> sheet 生图 -> 后端切图 -> 去背景/透明化 -> PNG 输出 -> OSS 持久化 -> 状态回写 -> 局部重生成 +``` + +玩法只提供: + +- `sheetSpec`:画布比例、行列、单格尺寸、输出格式、背景处理策略。 +- `slotSpecs`:每个素材槽位的 `slotId`、`slotType`、`slotName`、提示词、sheet 单元格映射。 +- 玩法字段映射:把通用素材结果映射回玩法自己的 draft/profile/runtime 字段。 + +通用系列素材结果建议字段: + +- `batchId` +- `slotId` +- `slotType` +- `slotName` +- `prompt` +- `imageSrc` +- `imageObjectKey` +- `assetObjectId` +- `sourceSheetCell` +- `status` - `error` -- `streamingReplyText` -- `selectionStage` 切换 -### Step 7: 定义 shared contracts +玩法可追加运行态字段,例如半径、宽度、视图索引或碰撞参数,但不能依赖任何玩法专属字段作为平台通用模型。新增玩法 compile action 内部调用通用系列素材服务;如果通用服务还缺能力,先补通用服务再接玩法。 -前端: -- `./Genarrative/packages/shared/src/contracts/` +### 8. 契约与 API -后端: -- `./Genarrative/server-rs/crates/shared-contracts/src/` +前后端必须同步补契约: -至少补齐: -- session snapshot -- create session request/response -- message request/response -- action request/response -- draft/result 结构 -- work summary / gallery 结构(如果需要) -- runtime 结构(如果需要) +- `packages/shared/src/contracts/` +- `server-rs/crates/shared-contracts/src/` -### Step 8: 实现前端 service client +玩法 API 保留独立命名空间: -目录参考: -- `./Genarrative/src/services/` +- `POST /api/creation//sessions` +- `GET /api/creation//sessions/{sessionId}` +- `POST /api/creation//sessions/{sessionId}/actions` +- `/api/creation//works` +- `/api/creation//runtime` -按玩法补: -- creation client -- runtime client(可选) -- works client(可选) -- gallery client(可选) +契约需要区分: -建议保持和现有玩法一致的 API base 与命名风格。 +- 工作台输入。 +- 草稿 snapshot。 +- 单图资产槽位。 +- 系列素材批次与槽位。 +- 结果页操作。 +- 发布作品摘要。 +- runtime snapshot。 -### Step 9: 接后端 API +### 9. 后端分层 -文件参考: -- `./Genarrative/server-rs/crates/api-server/src/puzzle.rs` -- `./Genarrative/server-rs/crates/api-server/src/puzzle_agent_turn.rs` -- `./Genarrative/server-rs/crates/api-server/src/match3d.rs` +按 DDD 边界落地: -通常需要: -- create session -- get session -- send message -- stream message -- execute action -- publish / save / delete -- runtime start / action(可选) -- gallery / detail(可选) +- `module-`:纯领域规则、状态机、draft/runtime 校验。 +- `shared-contracts`:前后端 DTO。 +- `spacetime-module`:表、reducer、procedure、事务编排、migration。 +- `spacetime-client`:typed facade 和 row mapper。 +- `api-server`:Axum 路由、鉴权、BFF、SSE、生成编排。 +- `platform-*`:LLM、图片生成、OSS、认证等外部副作用。 -后端设计优先按 Genarrative 的 DDD 分层拆开,不要把玩法规则、数据库事务、LLM 调用和 HTTP handler 混在一个文件里: -- `module-`:纯领域规则、状态机、draft/runtime 校验,不依赖 Axum、SpacetimeDB 或外部平台。 -- `shared-contracts`:前后端 DTO、请求/响应、session snapshot、draft/result/runtime 结构。 -- `spacetime-module`:表定义、reducer/procedure、事务编排、migration;表结构变化要同步生成绑定。 -- `spacetime-client`:api-server 到 SpacetimeDB 的 facade,隐藏 reducer 调用细节。 -- `api-server`:Axum 路由、鉴权、SSE/stream、应用层编排。 -- `platform-*`:LLM、资产上传、鉴权、第三方服务等副作用。 +涉及 SpacetimeDB schema 时同步 `migration.rs`、表目录和绑定,并运行 `npm run check:spacetime-schema`。 -建议按四条线设计后端能力: -- Agent 创作线:session、turn、stream、compile action。 -- Works 作品线:保存、发布、删除、草稿恢复。 -- Gallery 广场线:公开列表、详情、like/remix/share。 -- Runtime 运行态线:开始试玩、提交动作、读取状态。 - -### Step 10: 新增工作台组件 - -目录建议: -- `./Genarrative/src/components/-creation/AgentWorkspace.tsx` - -两种形态: - -#### 对话式 -适合设定逐轮补齐。 - -参考: -- `BigFishAgentWorkspace.tsx` -- `Match3DAgentWorkspace.tsx` - -#### 表单式 -适合输入结构明确的玩法。 - -参考: -- `PuzzleAgentWorkspace.tsx` - -### Step 11: 在渲染树中挂载新页面 - -文件: -- `./Genarrative/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx` - -补齐: -- workspace 分支 -- generating 分支(如需要) -- result 分支 -- runtime 分支(如需要) - -### Step 12: 新增结果页 - -目录建议: -- `./Genarrative/src/components/-result/ResultView.tsx` +### 10. 结果页 结果页至少支持: -- 展示 draft -- 返回编辑 -- 发布 -- 试玩 -- 错误展示 -### Step 13: 需要试玩就补 runtime +- 展示草稿和生成状态。 +- 返回工作台编辑。 +- 单图槽位重生成。 +- 系列素材追加、替换、局部重生成。 +- 发布。 +- 试玩。 +- 错误展示和失败重试。 -目录建议: -- `./Genarrative/src/components/-runtime/RuntimeShell.tsx` +单图槽位和系列素材槽位的生成状态互不阻塞。已有可查看结果时,局部重生成不能把作品架草稿重新变成不可打开的全局生成中。 -如果玩法是游戏类,建议补完整 runtime 闭环。 +### 11. 运行态、作品架和广场 -### Step 14: 接入作品架 / 广场 / 分享 +需要试玩或发布时补齐: -需要改: -- `./Genarrative/src/components/custom-world-home/creationWorkShelf.ts` -- `./Genarrative/src/components/custom-world-home/CustomWorldCreationHub.tsx` -- `./Genarrative/src/services/publicWorkCode.ts` +- runtime start/action/finish API。 +- 作品保存、发布、删除、回读。 +- 作品架摘要。 +- 公开列表、详情、分享码。 +- 公开列表优先消费后端投影或 BFF 缓存,不让前端直接拼源表事实。 -如果玩法支持发布,还要补: -- public work code -- public detail -- publish share modal -- like/remix(可选) +运行态可以做低延迟表现,但正式胜负、分数、奖励、排行榜和发布状态以后端裁决为准。 -### Step 15: 处理登录态与草稿恢复 +### 12. 恢复与登录态 -要考虑: -- 刷新恢复草稿 -- 退出登录清空私有状态 -- result/draft 缺失时回退 -- busy / generating / runtime 中断恢复 +必须处理: -### Step 16: 补测试 +- 刷新恢复生成中草稿。 +- 生成页计时从后端摘要时间恢复。 +- 失败后回读 session/work detail 再决定是否展示失败。 +- 退出登录清空私有玩法状态。 +- 私有生成图展示前换签。 +- result/runtime 缺必要 draft 时回到可恢复入口,不停在空白页。 -至少覆盖: -- 入口展示 -- 类型分流 -- 工作台打开 -- session 创建 -- compile action -- result 页切换 -- 发布后刷新作品架 -- runtime 进入与退出 +### 13. 例外流程 -## 最小改动清单 +任何非表单/图片工作台、对话式 Agent、独立创作系统或特殊资产模型都必须先更新 PRD 和平台文档。例外声明至少写清: -### 只做占位 +- 为什么默认表单/图片工作台不能满足。 +- 例外影响哪些输入、契约、后端流程和测试。 +- 如何保留单图资产槽位和系列素材槽位的通用能力。 +- 如何回退到平台默认链路。 -只改: -- `./Genarrative/src/config/newWorkEntryConfig.ts` +没有文档例外,不进入编码。 -### 做到可进入工作台 +## PRD 检查块 -至少改: -- `newWorkEntryConfig.ts` -- `platformEntryTypes.ts` -- `PlatformEntryFlowShellImpl.tsx` -- 新玩法 service client -- 新玩法工作台组件 -- shared contracts -- 后端 API +在新增玩法 PRD 中保留这一段: -### 做到完整闭环 +```md +## 创作工具平台接入声明 -还要补: -- result 页 -- runtime -- works / gallery -- public code -- share -- 作品架聚合 -- 测试 +- 工作台模式:表单/图片输入创作工作台 +- 创作链路:入口 -> 工作台 -> 生成页 -> 结果页 -> 试玩 -> 发布 -> 运行态 +- 单图资产槽位: + - slotId / slotType / slotName / 提示词来源 / 写回字段 / 是否允许历史图 / 是否允许 AI 重绘 +- 系列素材槽位: + - batchId / sheetSpec / slotSpecs / 切图规则 / 透明化规则 / 失败回写 / 局部重生成 +- API 命名空间:/api/creation//... +- 业务真相:后端裁决字段和前端表现字段边界 +- 创作工具模式例外:无;如有,先写明例外原因和回退方式 +- 验证命令: +``` -## 常见坑 +## 验证门禁 -1. 只加入口配置不够,类型分流和页面阶段也要补。 -2. `SelectionStage` 不扩展,前端无法安全切页。 -3. 新玩法如果要出现在作品架,必须改聚合逻辑,不只是加入口。 -4. 发布后不刷新 works/gallery,用户会看不到新作品。 -5. 如果走 SpacetimeDB,表结构变化要同步 migration 和绑定;`spacetime-client/src/module_bindings/` 通常是生成物,不要为了修编译或格式化而手改,优先改 module 源 schema/reducer/procedure 后重新生成。 -6. 做 analytics/tracking 这类 runtime 能力时,不要只补 API DTO;先在 `module-runtime` 写纯函数测试(例如 day/week/month/quarter/year bucket 聚合、scope/event 过滤),RED 后再补领域类型与聚合函数。 -7. 时间粒度聚合建议复用已有 date dimension 逻辑,把 daily stat 映射到 day/week/month/quarter/year bucket;bucket 输出要有稳定排序,并显式携带 `bucketKey`、`bucketStartDateKey`、`bucketEndDateKey`、`value`。 -8. 后端 shared-contracts 与前端 `packages/shared/src/contracts/runtime.ts` 要同步补 request/response/type union;admin-web 若有独立 `api/adminApiTypes.ts`,也要同步,避免共享包已更新但管理端本地类型缺失。 -9. 退出登录时要清空新玩法私有状态,避免串用户。 -10. 移动端入口卡片增多后要检查布局和滚动体验。 +按改动范围运行: -## 参考资料 - -- `references/genarrative-analytics-tracking-runtime.md`:analytics/tracking runtime 粒度聚合、contracts 同步与 SpacetimeDB 生成物注意事项。 - -## 验证标准 - -一个玩法算真正接入成功,至少要满足: - -- 入口能展示 -- 能进入对应工作台 -- 能创建 session -- 能生成草稿 -- 能进入结果页 -- 能返回编辑 -- 如果需要,可试玩 -- 如果需要,可发布 -- 发布后能回到作品架 / 广场 / 分享链路 +- `npm run check:encoding` +- `npm run typecheck` +- 前端工作台测试:确认没有聊天式 Agent 输入,提交的是表单/图片 payload。 +- `CreativeImageInputPanel` 测试:覆盖多玩法标签、上传、AI 重绘、参考图上限、历史图入口和删除确认。 +- 系列素材测试:覆盖 sheet layout、切图、透明化、OSS 持久化、追加、替换、局部重生成和失败回写。 +- 结果页测试:覆盖单图槽位重生成和系列素材槽位重生成互不阻塞。 +- 后端定向测试:覆盖 compile action、资产持久化、失败回写、发布和 runtime start。 +- 涉及 SpacetimeDB schema 时运行 `npm run check:spacetime-schema`。 diff --git a/CONTEXT.md b/CONTEXT.md index 6b9c8cf0..2937f24b 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -2,6 +2,20 @@ Genarrative 是一个 AI 原生互动内容与小游戏平台,当前上下文记录团队在玩法、作品、运行态和平台闭环中使用的领域语言。 +## 平台创作工具 + +**表单/图片输入创作工作台**: +新增玩法默认采用的创作工具模式,用户通过结构化表单、图片槽位和配置控件提交创作输入,链路覆盖入口、工作台、生成页、结果页、试玩、发布和运行态闭环。 +_Avoid_: 默认对话式 Agent 工作台、默认轻输入 Agent 工作台、复制既有玩法工作台 + +**单图资产编辑**: +角色形象、UI 背景、容器、封面、分享图等单张图资产的统一输入与重生成方式,统一通过 `CreativeImageInputPanel` 表达上传、AI 重绘、参考图、历史图和删除确认。 +_Avoid_: 在玩法页面内手写上传、参考图、重绘、预览、删除确认 + +**系列素材图集生成**: +一组同类素材的统一批量生成方式,采用批量规划、sheet 生图、后端切图、透明化、OSS 持久化和局部重生成的通用流水线。 +_Avoid_: 为每个玩法单独发明素材流水线、把系列素材建模成任一玩法专属 DTO + ## Language ### Bark Battle diff --git a/apps/admin-web/src/styles/admin.css b/apps/admin-web/src/styles/admin.css index 93ee4007..162c9678 100644 --- a/apps/admin-web/src/styles/admin.css +++ b/apps/admin-web/src/styles/admin.css @@ -1,6 +1,6 @@ :root { - color: #17212b; - background: #eef3f6; + color: #3d1f10; + background: #f8efe7; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; @@ -16,7 +16,7 @@ body { min-width: 320px; min-height: 100vh; margin: 0; - background: #eef3f6; + background: #f8efe7; } button, @@ -49,31 +49,31 @@ button:disabled { .admin-loading-screen { gap: 12px; - color: #5c6b77; + color: #7b6150; } .admin-loading-mark { width: 24px; height: 24px; - border: 3px solid #d1dde6; - border-top-color: #126e82; + border: 3px solid #e1ccbb; + border-top-color: #b6623f; border-radius: 50%; animation: admin-spin 0.8s linear infinite; } .admin-login-screen { background: - linear-gradient(145deg, rgba(18, 110, 130, 0.12), transparent 36%), - linear-gradient(315deg, rgba(165, 94, 54, 0.12), transparent 34%), - #eef3f6; + linear-gradient(145deg, rgba(204, 117, 76, 0.14), transparent 36%), + linear-gradient(315deg, rgba(226, 171, 134, 0.16), transparent 34%), + #f8efe7; } .admin-login-panel, .admin-panel { - border: 1px solid #d8e2e8; + border: 1px solid #e1ccbb; border-radius: 8px; background: #ffffff; - box-shadow: 0 12px 36px rgba(23, 33, 43, 0.08); + box-shadow: 0 12px 36px rgba(112, 57, 30, 0.08); } .admin-login-panel { @@ -92,14 +92,14 @@ button:disabled { .admin-login-brand h1 { margin: 0; - color: #17212b; + color: #3d1f10; font-size: 26px; line-height: 1.16; } .admin-login-brand span, .admin-brand span { - color: #667682; + color: #8f7868; font-size: 12px; } @@ -108,10 +108,10 @@ button:disabled { width: 38px; height: 38px; place-items: center; - border: 1px solid #bcd2db; + border: 1px solid #dfc8b7; border-radius: 8px; - color: #126e82; - background: #e7f3f5; + color: #b6623f; + background: #f4e5d7; } .admin-brand-icon-large { @@ -129,7 +129,7 @@ button:disabled { display: flex; flex-direction: column; gap: 24px; - border-right: 1px solid #d8e2e8; + border-right: 1px solid #e1ccbb; background: #ffffff; padding: 22px 18px; } @@ -164,13 +164,13 @@ button:disabled { gap: 10px; min-height: 42px; padding: 0 12px; - color: #52616d; + color: #755a49; background: transparent; } .admin-nav-button[data-active="true"] { - color: #0f5666; - background: #e7f3f5; + color: #8f3f27; + background: #f4e5d7; } .admin-main { @@ -184,7 +184,7 @@ button:disabled { align-items: center; justify-content: flex-end; gap: 12px; - border-bottom: 1px solid #d8e2e8; + border-bottom: 1px solid #e1ccbb; background: rgba(255, 255, 255, 0.86); padding: 0 24px; } @@ -200,7 +200,7 @@ button:disabled { } .admin-user small { - color: #667682; + color: #8f7868; } .admin-content { @@ -232,7 +232,7 @@ button:disabled { .admin-page-heading h2, .admin-panel-heading h3 { margin: 0; - color: #17212b; + color: #3d1f10; } .admin-page-heading h2 { @@ -241,7 +241,7 @@ button:disabled { .admin-page-heading p { margin: 3px 0 0; - color: #667682; + color: #8f7868; } .admin-panel { @@ -256,7 +256,7 @@ button:disabled { } .admin-panel-heading span { - color: #667682; + color: #8f7868; font-size: 13px; } @@ -314,12 +314,12 @@ button:disabled { } .admin-table tbody tr[data-clickable="true"]:hover { - background: #f5fafb; + background: #fff7f0; } .admin-text-button:hover, .admin-text-button:focus-visible { - color: #126e82; + color: #b6623f; text-decoration: underline; outline: none; } @@ -345,7 +345,7 @@ button:disabled { } .admin-query-summary { - color: #667682; + color: #8f7868; font-size: 12px; font-weight: 650; } @@ -354,7 +354,7 @@ button:disabled { display: grid; min-width: 0; gap: 7px; - color: #4c5c68; + color: #6f5848; font-size: 13px; font-weight: 650; } @@ -372,16 +372,16 @@ button:disabled { .admin-field textarea { width: 100%; min-height: 42px; - border: 1px solid #cbd8e0; + border: 1px solid #dfc8b7; border-radius: 8px; - color: #17212b; - background: #fbfdfe; + color: #3d1f10; + background: #fffdf9; padding: 9px 11px; outline: none; } .admin-field-note { - color: #667682; + color: #8f7868; font-size: 12px; font-weight: 500; line-height: 1.45; @@ -395,8 +395,8 @@ button:disabled { .admin-field input:focus, .admin-field select:focus, .admin-field textarea:focus { - border-color: #126e82; - box-shadow: 0 0 0 3px rgba(18, 110, 130, 0.16); + border-color: #b6623f; + box-shadow: 0 0 0 3px rgba(204, 117, 76, 0.16); } .admin-combobox { @@ -419,16 +419,16 @@ button:disabled { align-items: center; justify-content: center; min-height: 42px; - border: 1px solid #cbd8e0; + border: 1px solid #dfc8b7; border-left: 0; border-radius: 0 8px 8px 0; - color: #52616d; - background: #fbfdfe; + color: #755a49; + background: #fffdf9; } .admin-combobox:focus-within .admin-combobox-toggle { - border-color: #126e82; - box-shadow: 0 0 0 3px rgba(18, 110, 130, 0.16); + border-color: #b6623f; + box-shadow: 0 0 0 3px rgba(204, 117, 76, 0.16); } .admin-combobox-menu { @@ -440,10 +440,10 @@ button:disabled { display: grid; max-height: 260px; overflow: auto; - border: 1px solid #cbd8e0; + border: 1px solid #dfc8b7; border-radius: 8px; background: #ffffff; - box-shadow: 0 16px 40px rgba(23, 33, 43, 0.14); + box-shadow: 0 16px 40px rgba(112, 57, 30, 0.14); padding: 6px; } @@ -453,7 +453,7 @@ button:disabled { width: 100%; border: 0; border-radius: 7px; - color: #17212b; + color: #3d1f10; background: transparent; padding: 9px 10px; text-align: left; @@ -461,12 +461,12 @@ button:disabled { .admin-combobox-option:hover, .admin-combobox-option:focus-visible { - background: #e7f3f5; + background: #f4e5d7; outline: none; } .admin-combobox-option span { - color: #0f5666; + color: #8f3f27; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 12px; @@ -475,7 +475,7 @@ button:disabled { .admin-combobox-option small, .admin-combobox-empty { - color: #667682; + color: #8f7868; font-size: 12px; font-weight: 500; line-height: 1.45; @@ -495,7 +495,7 @@ button:disabled { justify-content: flex-end; gap: 8px; min-height: 42px; - color: #4c5c68; + color: #6f5848; font-size: 13px; font-weight: 650; } @@ -503,7 +503,7 @@ button:disabled { .admin-switch-field input { width: 18px; height: 18px; - accent-color: #126e82; + accent-color: #b6623f; } .admin-primary-button, @@ -522,7 +522,7 @@ button:disabled { z-index: 80; display: grid; place-items: center; - background: rgba(23, 33, 43, 0.42); + background: rgba(61, 31, 16, 0.34); padding: 16px; } @@ -530,10 +530,10 @@ button:disabled { display: grid; width: min(100%, 420px); gap: 16px; - border: 1px solid #d8e2e8; + border: 1px solid #e1ccbb; border-radius: 10px; background: #ffffff; - box-shadow: 0 22px 60px rgba(23, 33, 43, 0.24); + box-shadow: 0 22px 60px rgba(112, 57, 30, 0.24); padding: 18px; } @@ -543,10 +543,10 @@ button:disabled { max-height: min(90dvh, 760px); gap: 16px; overflow: auto; - border: 1px solid #d8e2e8; + border: 1px solid #e1ccbb; border-radius: 10px; background: #ffffff; - box-shadow: 0 22px 60px rgba(23, 33, 43, 0.24); + box-shadow: 0 22px 60px rgba(112, 57, 30, 0.24); padding: 18px; } @@ -557,7 +557,7 @@ button:disabled { .admin-confirm-warning { border: 1px solid #efc894; border-radius: 8px; - color: #8a5a1b; + color: #8f4b26; background: #fffaf3; padding: 10px 12px; font-size: 13px; @@ -576,26 +576,26 @@ button:disabled { .admin-primary-button { color: #ffffff; - background: #126e82; + background: #b6623f; } .admin-secondary-button, .admin-icon-button { - border: 1px solid #cbd8e0; - color: #2f4550; + border: 1px solid #dfc8b7; + color: #4b2412; background: #ffffff; } .admin-danger-button { color: #ffffff; - background: #a44242; + background: #a6402f; } .admin-ghost-button { width: 34px; height: 34px; - color: #52616d; - background: #eef3f6; + color: #755a49; + background: #f8efe7; } .admin-ghost-button.admin-query-reset-button { @@ -608,7 +608,7 @@ button:disabled { .admin-text-button { display: inline; border: 0; - color: #0f5666; + color: #8f3f27; background: transparent; padding: 0; text-align: left; @@ -616,9 +616,9 @@ button:disabled { } .admin-alert { - border: 1px solid #efc0bd; + border: 1px solid #e2b9a4; border-radius: 8px; - color: #8a2f2f; + color: #8f3f27; background: #fff4f3; padding: 10px 12px; font-size: 13px; @@ -638,7 +638,7 @@ button:disabled { } .admin-info-list dt { - color: #667682; + color: #8f7868; font-size: 12px; } @@ -646,7 +646,7 @@ button:disabled { min-width: 0; margin: 0; overflow-wrap: anywhere; - color: #17212b; + color: #3d1f10; font-size: 13px; font-weight: 650; } @@ -666,26 +666,26 @@ button:disabled { .admin-table th, .admin-table td { - border-bottom: 1px solid #e4edf2; + border-bottom: 1px solid #eaded2; padding: 10px; text-align: left; vertical-align: top; } .admin-table th { - color: #667682; + color: #8f7868; font-size: 12px; } .admin-table td small { display: block; margin-top: 3px; - color: #667682; + color: #8f7868; font-size: 12px; } .admin-muted-text { - color: #86939c; + color: #a38f80; } .admin-tag-list { @@ -699,10 +699,10 @@ button:disabled { display: inline-flex; max-width: 100%; align-items: center; - border: 1px solid #cbdfe6; + border: 1px solid #dfc8b7; border-radius: 999px; - background: #eef7f8; - color: #0f5666; + background: #f7eadf; + color: #8f3f27; padding: 3px 8px; font-size: 12px; font-weight: 750; @@ -744,7 +744,7 @@ button:disabled { gap: 6px; max-width: 100%; border: 0; - color: #667682; + color: #8f7868; background: transparent; padding: 0; text-align: left; @@ -773,7 +773,7 @@ button:disabled { .admin-table-sort-button:hover, .admin-table-sort-button:focus-visible, .admin-table-sort-button[data-active="true"] { - color: #0f5666; + color: #8f3f27; outline: none; } @@ -782,7 +782,7 @@ button:disabled { max-height: 160px; margin: 0; overflow: auto; - color: #2f4550; + color: #4b2412; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 12px; @@ -801,18 +801,18 @@ button:disabled { } .admin-status-ok { - color: #17623c; - background: #e6f5ed; + color: #2f7b46; + background: #edf8ef; } .admin-status-pending { - color: #8a5a1b; - background: #fff4df; + color: #8f4b26; + background: #fdf1e5; } .admin-status-error { - color: #8a2f2f; - background: #fff1ef; + color: #8f3f27; + background: #fff0e9; } .admin-error-list { @@ -820,7 +820,7 @@ button:disabled { gap: 8px; margin: 0; padding-left: 18px; - color: #8a5a1b; + color: #8f4b26; overflow-wrap: anywhere; } @@ -830,7 +830,7 @@ button:disabled { } .admin-subsection-heading { - color: #4c5c68; + color: #6f5848; font-size: 13px; font-weight: 650; } @@ -850,7 +850,7 @@ button:disabled { .admin-header-row input { min-width: 0; min-height: 38px; - border: 1px solid #cbd8e0; + border: 1px solid #dfc8b7; border-radius: 8px; padding: 8px 10px; } @@ -863,10 +863,10 @@ button:disabled { max-height: 520px; margin: 0; overflow: auto; - border: 1px solid #dce6ec; + border: 1px solid #eaded2; border-radius: 8px; - background: #17212b; - color: #e9f2f4; + background: #3d1f10; + color: #fdf9f5; padding: 14px; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; @@ -880,19 +880,19 @@ button:disabled { display: grid; min-height: 140px; place-items: center; - border: 1px dashed #cbd8e0; + border: 1px dashed #dfc8b7; border-radius: 8px; - color: #667682; - background: #fbfdfe; + color: #8f7868; + background: #fffdf9; } .admin-segmented-control { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; - border: 1px solid #d8e2e8; + border: 1px solid #e1ccbb; border-radius: 8px; - background: #eef3f6; + background: #f8efe7; padding: 4px; } @@ -900,15 +900,15 @@ button:disabled { min-height: 36px; border: 0; border-radius: 6px; - color: #52616d; + color: #755a49; background: transparent; font-weight: 700; } .admin-segmented-control button[data-active="true"] { - color: #0f5666; + color: #8f3f27; background: #ffffff; - box-shadow: 0 2px 8px rgba(23, 33, 43, 0.08); + box-shadow: 0 2px 8px rgba(112, 57, 30, 0.08); } .admin-bottom-nav { @@ -964,7 +964,7 @@ button:disabled { z-index: 20; display: grid; grid-template-columns: repeat(auto-fit, minmax(64px, 1fr)); - border-top: 1px solid #d8e2e8; + border-top: 1px solid #e1ccbb; background: rgba(255, 255, 255, 0.94); padding: 8px 10px calc(8px + env(safe-area-inset-bottom)); backdrop-filter: blur(10px); @@ -974,15 +974,15 @@ button:disabled { display: grid; gap: 4px; min-height: 48px; - color: #667682; + color: #8f7868; background: transparent; font-size: 11px; font-weight: 700; } .admin-bottom-nav-button[data-active="true"] { - color: #0f5666; - background: #e7f3f5; + color: #8f3f27; + background: #f4e5d7; } } diff --git a/bash.exe.stackdump b/bash.exe.stackdump deleted file mode 100644 index eb999467..00000000 --- a/bash.exe.stackdump +++ /dev/null @@ -1,28 +0,0 @@ -Stack trace: -Frame Function Args -0007FFFFB520 00021005FE8E (000210285F68, 00021026AB6E, 000000000000, 0007FFFFA420) msys-2.0.dll+0x1FE8E -0007FFFFB520 0002100467F9 (000000000000, 000000000000, 000000000000, 0007FFFFB7F8) msys-2.0.dll+0x67F9 -0007FFFFB520 000210046832 (000210286019, 0007FFFFB3D8, 000000000000, 000000000000) msys-2.0.dll+0x6832 -0007FFFFB520 000210068CF6 (000000000000, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x28CF6 -0007FFFFB520 000210068E24 (0007FFFFB530, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x28E24 -0007FFFFB800 00021006A225 (0007FFFFB530, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x2A225 -End of stack trace -Loaded modules: -000100400000 bash.exe -7FFA3C060000 ntdll.dll -7FFA3B490000 KERNEL32.DLL -7FFA390F0000 KERNELBASE.dll -7FFA3BE50000 USER32.dll -7FFA38E90000 win32u.dll -7FFA3A230000 GDI32.dll -7FFA38D60000 gdi32full.dll -7FFA38EC0000 msvcp_win.dll -7FFA38930000 ucrtbase.dll -000210040000 msys-2.0.dll -7FFA39EB0000 advapi32.dll -7FFA3A180000 msvcrt.dll -7FFA3BCA0000 sechost.dll -7FFA3B5F0000 RPCRT4.dll -7FFA37D70000 CRYPTBASE.DLL -7FFA38B40000 bcryptPrimitives.dll -7FFA3A260000 IMM32.DLL diff --git a/deploy/container/nginx.conf b/deploy/container/nginx.conf index 2799af16..239b5c4c 100644 --- a/deploy/container/nginx.conf +++ b/deploy/container/nginx.conf @@ -170,6 +170,8 @@ http { location ~ ^/api(?:/|$) { default_type application/json; + # 中文注释:创作接口会携带参考图 Data URL,Nginx 只放行到 api-server;真实大小限制仍由路由 DefaultBodyLimit 和业务字节校验负责。 + client_max_body_size 64m; limit_conn genarrative_api_conn 64; limit_req zone=genarrative_api_rps burst=64 nodelay; diff --git a/deploy/nginx/README.md b/deploy/nginx/README.md index 2dfa2110..054734de 100644 --- a/deploy/nginx/README.md +++ b/deploy/nginx/README.md @@ -2,6 +2,12 @@ 本配置片段由 `scripts/jenkins-server-provision.sh` 在安装 Nginx 站点配置时展开。 +## 请求体大小 + +- 生产、开发服和容器模板都在通用 `location ~ ^/api(?:/|$)` 内设置 `client_max_body_size 64m`。 +- 该值只用于让携带参考图 Data URL 的创作接口抵达 `api-server`;不要把它当作业务上传上限。Rust 路由仍通过 `DefaultBodyLimit` 和解码后字节校验限制具体接口,例如拼图参考图路由只放宽到 12 MiB 请求体,图片字节继续按业务规则拒绝。 +- 若线上看到 `413 Request Entity Too Large`,并且 access log 里 `request_time=0.000 upstream_status=-`,通常是 Nginx 没有加载该模板或未 reload;先执行 `nginx -T | grep client_max_body_size` 和 `nginx -t` 再检查 `api-server`。 + ## gzip - `deploy/nginx/genarrative.conf` 与 `deploy/nginx/genarrative-dev-http.conf` 默认开启 gzip。 diff --git a/deploy/nginx/genarrative-dev-http.conf b/deploy/nginx/genarrative-dev-http.conf index 63234e30..b7c0cdaa 100644 --- a/deploy/nginx/genarrative-dev-http.conf +++ b/deploy/nginx/genarrative-dev-http.conf @@ -190,6 +190,8 @@ server { # 临时兼容主站仍在使用的 /api/* HTTP facade;前端完成 SpacetimeDB SDK 迁移后删除。 location ~ ^/api(?:/|$) { default_type application/json; + # 中文注释:创作接口会携带参考图 Data URL,Nginx 只放行到 api-server;真实大小限制仍由路由 DefaultBodyLimit 和业务字节校验负责。 + client_max_body_size 64m; limit_conn genarrative_api_conn 64; limit_req zone=genarrative_api_rps burst=64 nodelay; diff --git a/deploy/nginx/genarrative.conf b/deploy/nginx/genarrative.conf index 023a96f8..c26e9bbb 100644 --- a/deploy/nginx/genarrative.conf +++ b/deploy/nginx/genarrative.conf @@ -210,6 +210,8 @@ server { # 临时兼容主站仍在使用的 /api/* HTTP facade;前端完成 SpacetimeDB SDK 迁移后删除。 location ~ ^/api(?:/|$) { default_type application/json; + # 中文注释:创作接口会携带参考图 Data URL,Nginx 只放行到 api-server;真实大小限制仍由路由 DefaultBodyLimit 和业务字节校验负责。 + client_max_body_size 64m; limit_conn genarrative_api_conn 64; limit_req zone=genarrative_api_rps burst=64 nodelay; diff --git a/docs/README.md b/docs/README.md index 2ae24fda..36689c78 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ 重点补充:RPG 创作与运行时脚本职责地图见 [RPG_CREATION_AND_RUNTIME_SCRIPT_RESPONSIBILITY_MAP_2026-04-28.md](./reference/RPG_CREATION_AND_RUNTIME_SCRIPT_RESPONSIBILITY_MAP_2026-04-28.md)。 - [埋点查询](./tracking/README.md):埋点原始事件与聚合投影的本地 SQL 查询。 - [运营查询](./operations/README.md):任务、领奖、钱包对账等后台核查查询。 -- [PRD](./prd/README.md):产品需求与阶段计划;参考 MOKU / 幕间类 AI 文游的陶泥儿 `text-game` 模板口径见 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_2026-05-05.md](./prd/AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_2026-05-05.md),视觉小说模板 TXT 玩法平台化接入口径见 [AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md](./prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md),创意互动内容 Agent Phase 1 的 LangChain-Rust PoC、拼图闭环和并行任务拆分见 [CREATIVE_INTERACTIVE_AGENT_PHASE1_LANGCHAIN_RUST_PUZZLE_LOOP_PRD_2026-05-05.md](./prd/CREATIVE_INTERACTIVE_AGENT_PHASE1_LANGCHAIN_RUST_PUZZLE_LOOP_PRD_2026-05-05.md),幸存者类模板闭环见 [AI_NATIVE_SURVIVOR_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-05.md](./prd/AI_NATIVE_SURVIVOR_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-05.md),后台管理独立前端工程见 [ADMIN_WEB_CONSOLE_PRD_2026-04-30.md](./prd/ADMIN_WEB_CONSOLE_PRD_2026-04-30.md),新增 RPG 开场动画方案见 [AI_NATIVE_RPG_OPENING_ANIMATION_PRD_2026-04-25.md](./prd/AI_NATIVE_RPG_OPENING_ANIMATION_PRD_2026-04-25.md),新增抓大鹅 Match3D 玩法方案见 [AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md](./prd/AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md),方洞挑战创作、发布与试玩闭环见 [AI_NATIVE_SQUARE_HOLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-04.md](./prd/AI_NATIVE_SQUARE_HOLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-04.md)。 +- [PRD](./prd/README.md):产品需求与阶段计划;参考 MOKU / 幕间类 AI 文游的陶泥儿 `text-game` 模板口径见 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_2026-05-05.md](./prd/AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_2026-05-05.md),视觉小说模板 TXT 玩法平台化接入口径见 [AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md](./prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md),创意互动内容 Agent Phase 1 的 LangChain-Rust PoC、拼图闭环和并行任务拆分见 [CREATIVE_INTERACTIVE_AGENT_PHASE1_LANGCHAIN_RUST_PUZZLE_LOOP_PRD_2026-05-05.md](./prd/CREATIVE_INTERACTIVE_AGENT_PHASE1_LANGCHAIN_RUST_PUZZLE_LOOP_PRD_2026-05-05.md),幸存者类模板闭环见 [AI_NATIVE_SURVIVOR_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-05.md](./prd/AI_NATIVE_SURVIVOR_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-05.md),后台管理独立前端工程见 [ADMIN_WEB_CONSOLE_PRD_2026-04-30.md](./prd/ADMIN_WEB_CONSOLE_PRD_2026-04-30.md),新增 RPG 开场动画方案见 [AI_NATIVE_RPG_OPENING_ANIMATION_PRD_2026-04-25.md](./prd/AI_NATIVE_RPG_OPENING_ANIMATION_PRD_2026-04-25.md),新增抓大鹅 Match3D 玩法方案见 [AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md](./prd/AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md),方洞挑战创作、发布与试玩闭环见 [AI_NATIVE_SQUARE_HOLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-04.md](./prd/AI_NATIVE_SQUARE_HOLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-05-04.md),跳一跳俯视角玩法模板 PRD 见 [【玩法创作】跳一跳俯视角玩法模板PRD-2026-05-19.md](./prd/%E3%80%90%E7%8E%A9%E6%B3%95%E5%88%9B%E4%BD%9C%E3%80%91%E8%B7%B3%E4%B8%80%E8%B7%B3%E4%BF%AF%E8%A7%86%E8%A7%92%E7%8E%A9%E6%B3%95%E6%A8%A1%E6%9D%BFPRD-2026-05-19.md)。 生产部署切换到 systemd + Nginx + SpacetimeDB 自托管的总方案见 [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md),该文档也是当前生产 Jenkinsfile 的唯一入口。SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md);private 表迁移 JSON 导入导出、HTTP 413 分片导入和旧数据库迁移流水线经验见 [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md) 与 [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md);后台管理独立前端工程技术方案见 [ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md](./technical/ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md)。 diff --git a/docs/prd/BABY_OBJECT_MATCH_EDUTAINMENT_TEMPLATE_PRD_2026-05-11.md b/docs/prd/BABY_OBJECT_MATCH_EDUTAINMENT_TEMPLATE_PRD_2026-05-11.md index ca2cedf0..95c74dd9 100644 --- a/docs/prd/BABY_OBJECT_MATCH_EDUTAINMENT_TEMPLATE_PRD_2026-05-11.md +++ b/docs/prd/BABY_OBJECT_MATCH_EDUTAINMENT_TEMPLATE_PRD_2026-05-11.md @@ -1,4 +1,4 @@ -# 宝贝识物寓教于乐模板 PRD 2026-05-11 +# 宝贝识物寓教于乐模板 PRD 2026-05-11 ## 1. 目标 @@ -32,7 +32,7 @@ 5. 游戏视觉主题包; 6. 作品标签。 -素材使用 VectorEngine `gpt-image-2-all` / image-2 生成。图片生成只能走后端接口,前端不得读取、拼接或暴露 `VECTOR_ENGINE_API_KEY`。 +素材使用 VectorEngine `gpt-image-2` / image-2 生成。图片生成只能走后端接口,前端不得读取、拼接或暴露 `VECTOR_ENGINE_API_KEY`。 为降低生成成本,创作提交后只生成两张原始图片:一张 `2x2` 素材 sheet 和一张单独场景背景图。`2x2` 素材 sheet 固定包含左上物品 A、右上物品 B、左下篮子、右下礼物盒。服务端必须按固定格切图,并把物品、篮子和礼物盒转成透明 PNG。只有透明抠图后的两个物品素材才允许写入草稿 `itemAssets` 并进入游戏运行态。左右手位置指示器属于运行态默认规则,使用项目内置静态素材,不在每次创作时生成。 diff --git a/docs/prd/【玩法创作】跳一跳俯视角玩法模板PRD-2026-05-19.md b/docs/prd/【玩法创作】跳一跳俯视角玩法模板PRD-2026-05-19.md new file mode 100644 index 00000000..63af3568 --- /dev/null +++ b/docs/prd/【玩法创作】跳一跳俯视角玩法模板PRD-2026-05-19.md @@ -0,0 +1,485 @@ +# 跳一跳俯视角玩法模板 PRD 2026-05-19 + +## 1. 目标 + +新增一个可创作、可试玩、可发布的玩法模板: + +```text +跳一跳 +``` + +本模板参考拼图模板的创作闭环,沿用“创作入口 -> 生成过程页 -> 结果页 -> 试玩 -> 发布”的平台链路,但玩法本体改为俯视角 / 等距视角的跳跃闯关。 + +首版要求: + +1. 初始草稿生成时,角色形象单独调用一次生图; +2. 初始草稿生成时,地块只调用一次生图,输出 3D 视图的 2D 图片图集; +3. 运行态不接真实 3D 网格,不生成 GLB / glTF; +4. 作品可以直接进入试玩和发布。 + +## 2. 模板定位 + +模板 ID: + +```text +jump-hop +``` + +用户展示名: + +```text +跳一跳 +``` + +体验关键词: + +1. 俯视角; +2. 等距感地块; +3. 单局闯关; +4. 长按蓄力,松手起跳; +5. 轻量休闲。 + +首版采用竖屏优先的移动端体验,桌面端保持居中展示,画面比例以 `9:16` 为主。参考图的核心视觉要点是: + +1. 大面积留白或浅色渐变背景; +2. 角色站在单个地块上; +3. 地块有明显顶面、侧面和投影; +4. 整体是俯视角 / 等距视角,而不是横版平台跳跃; +5. UI 克制,只保留必要控制,不堆说明文案。 + +## 3. 与拼图模板的复用边界 + +可以复用: + +1. 创作入口和模板分流; +2. 生成过程页; +3. 结果页的草稿保存、返回编辑、试玩、发布、分享链路; +4. 作品架展示和草稿恢复口径; +5. 平台统一的发布与公开展示流程。 + +不复用: + +1. 拼图关卡切片逻辑; +2. 拼图拖拽拼块逻辑; +3. 拼图 UI 背景和多关卡编辑结构; +4. 任何方格拼合语义。 + +## 4. 工程接入范围 + +首版需要做到完整玩法闭环,不只做入口占位。 + +新增前端阶段: + +```text +jump-hop-workspace +jump-hop-generating +jump-hop-result +jump-hop-runtime +jump-hop-gallery-detail +``` + +新增前端组件建议: + +1. `src/components/jump-hop-creation/JumpHopWorkspace.tsx`; +2. `src/components/jump-hop-result/JumpHopResultView.tsx`; +3. `src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx`; +4. `src/services/jump-hop/jumpHopClient.ts`。 + +新增共享契约建议: + +1. `packages/shared/src/contracts/jumpHop.ts`; +2. `server-rs/crates/shared-contracts/src/jump_hop.rs`。 + +新增后端模块建议: + +1. `server-rs/crates/module-jump-hop`:纯领域规则,包含路径生成、蓄力换算、落点判定、通关 / 失败状态机; +2. `server-rs/crates/api-server/src/jump_hop.rs` 和 `src/jump_hop/` 子模块:HTTP handler、生成编排、资产保存和 DTO 映射; +3. `server-rs/crates/spacetime-module/src/jump_hop.rs`:session、work profile、runtime run、公开 view 和 reducer / procedure; +4. `server-rs/crates/spacetime-client/src/jump_hop.rs`:api-server 访问 SpacetimeDB 的 facade; +5. `server-rs/crates/api-server/src/modules/jump_hop.rs`:路由挂载。 + +入口配置事实源必须走 SpacetimeDB `creation_entry_type_config` 默认种子和后台配置接口,不新增前端硬编码入口配置。 + +## 5. 创作输入 + +创作者需要填写以下内容: + +1. 作品主题描述,必填; +2. 角色形象描述,必填; +3. 地块风格卡,必选; +4. 难度,必选; +5. 可选的终点氛围或节奏偏好。 + +推荐的最小输入形态是: + +1. 一句话主题; +2. 角色一句话描述; +3. 风格卡; +4. 难度卡。 + +不在首版开放手工拖拽平台编辑器。平台路径、地块间距和终点位置由系统自动生成,创作者只负责风格与难度选择。 + +### 5.1 地块风格卡 + +建议提供以下风格: + +1. 极简积木; +2. 纸模玩具; +3. 霓虹玻璃; +4. 森林石块; +5. 未来金属; +6. 自定义。 + +### 5.2 难度 + +建议提供以下离散档位: + +1. 轻松; +2. 标准; +3. 进阶; +4. 挑战。 + +难度主要影响: + +1. 平台路径长度; +2. 平台间距; +3. 可落点容差; +4. 完美落点窗口; +5. 终点前的节奏变化。 + +## 6. 生成规则 + +本模板必须把生图责任拆成两条独立链路: + +### 6.1 角色形象只生一次 + +角色形象必须只调用一次生图,输出一张可直接进入运行态的主角色图。 + +角色图要求: + +1. 单人主角; +2. 全身可见; +3. 透明背景; +4. 角色站姿或轻微前倾姿态; +5. 镜头和透视必须匹配俯视角场景; +6. 不要求多视角,不要求多帧动画图集。 + +角色图生成后作为作品级锚点资产使用,结果页、封面合成、试玩和发布都复用同一张图。后续如果只修改标题、标签、难度或路径,不应默认重新生角色。只有用户在结果页明确点击“重生成角色”时,才允许再调用一次角色生图。 + +### 6.2 地块只生一次图集 + +地块必须只调用一次生图,输出一张 3D 视图的 2D 图片图集,再由后端切成运行态可用的地块资产。 + +地块图集要求: + +1. 统一使用等距 / 俯视角; +2. 必须表现出顶面、侧面和投影; +3. 必须与角色图保持同一光向; +4. 必须有清晰的立体层次,但仍然是 2D 图片; +5. 必须包含至少以下地块类型: + - 起点地块; + - 普通地块; + - 目标地块; + - 终点地块。 + +建议额外包含: + +1. 奖励地块; +2. 视觉强调地块; +3. 风格化变体地块。 + +图集生成后按地块类型切分并去掉背景,运行态直接消费切好的 PNG,不在前端做复杂拼接。只有用户在结果页明确点击“重生成地块”时,才允许再调用一次地块图集生图。 + +### 6.3 不新增第三次生成 + +首版不把封面、分享海报、路径预览再拆成第三次图像生成。封面和分享图必须由角色图 + 地块图集在本地或后端轻量合成,不额外增加新的角色生图次数。 + +### 6.4 路径元数据 + +除图片资产外,系统还必须生成跳跃路径元数据: + +1. 平台序列; +2. 平台中心点; +3. 平台宽度; +4. 平台间距; +5. 终点索引; +6. 评分和容差参数。 + +路径由领域规则自动生成,创作者不直接编辑坐标。路径元数据不依赖 LLM 或图片生成。 + +### 6.5 推荐的难度区间 + +| 难度 | 平台数量 | 平台间距 | 节奏 | +| --- | ---: | --- | --- | +| 轻松 | 12 - 14 | 短 | 宽容 | +| 标准 | 16 - 18 | 中 | 稳定 | +| 进阶 | 20 - 24 | 中长 | 紧凑 | +| 挑战 | 26 - 32 | 长 | 高压 | + +平台宽度和容差由系统按难度自动缩放,不要求创作者手工填写。 + +## 7. 契约草案 + +### 7.1 草稿结构 + +`JumpHopDraft` 至少包含: + +1. `templateId = "jump-hop"`; +2. `templateName = "跳一跳"`; +3. `profileId`; +4. `workTitle`; +5. `workDescription`; +6. `themeTags`; +7. `difficulty`; +8. `stylePreset`; +9. `characterPrompt`; +10. `tilePrompt`; +11. `characterAsset`; +12. `tileAtlasAsset`; +13. `tileAssets[]`; +14. `path`; +15. `coverComposite`; +16. `generationStatus`。 + +### 7.2 资产结构 + +`JumpHopCharacterAsset` 至少包含: + +1. `assetId`; +2. `imageSrc`; +3. `imageObjectKey`; +4. `assetObjectId`; +5. `generationProvider`; +6. `prompt`; +7. `width`; +8. `height`。 + +`JumpHopTileAsset` 至少包含: + +1. `tileType`; +2. `imageSrc`; +3. `imageObjectKey`; +4. `assetObjectId`; +5. `sourceAtlasCell`; +6. `visualWidth`; +7. `visualHeight`; +8. `topSurfaceRadius`; +9. `landingRadius`。 + +`tileType` 首版限定: + +```text +start | normal | target | finish | bonus | accent +``` + +### 7.3 路径结构 + +`JumpHopPath` 至少包含: + +1. `seed`; +2. `difficulty`; +3. `platforms[]`; +4. `finishIndex`; +5. `cameraPreset`; +6. `scoring`。 + +`JumpHopPlatform` 至少包含: + +1. `platformId`; +2. `tileType`; +3. `x`; +4. `y`; +5. `width`; +6. `height`; +7. `landingRadius`; +8. `perfectRadius`; +9. `scoreValue`。 + +### 7.4 运行态快照 + +`JumpHopRunSnapshot` 至少包含: + +1. `runId`; +2. `profileId`; +3. `status = playing | failed | cleared`; +4. `currentPlatformIndex`; +5. `score`; +6. `combo`; +7. `lastJump`; +8. `startedAtMs`; +9. `finishedAtMs`。 + +`lastJump` 至少包含: + +1. `chargeMs`; +2. `jumpDistance`; +3. `targetPlatformIndex`; +4. `landedX`; +5. `landedY`; +6. `result = miss | hit | perfect | finish`。 + +## 8. API 草案 + +HTTP 路由建议: + +```text +POST /api/creation/jump-hop/sessions +GET /api/creation/jump-hop/sessions/{sessionId} +POST /api/creation/jump-hop/sessions/{sessionId}/actions +POST /api/creation/jump-hop/works/{profileId}/publish +GET /api/runtime/jump-hop/works/{profileId} +POST /api/runtime/jump-hop/runs +POST /api/runtime/jump-hop/runs/{runId}/jump +POST /api/runtime/jump-hop/runs/{runId}/restart +GET /api/runtime/jump-hop/gallery +GET /api/runtime/jump-hop/gallery/{publicWorkCode} +``` + +动作类型建议: + +```text +compile-draft +regenerate-character +regenerate-tiles +update-work-meta +update-difficulty +``` + +`compile-draft` 是长耗时动作。前端进入生成页后必须持久化 `generationStatus=generating`,刷新后能从作品架恢复生成页。失败前需要复读 session;如果后端已经完成草稿并写回资产,前端按成功收尾。 + +## 9. SpacetimeDB 表和 view + +建议新增表: + +1. `jump_hop_agent_session`; +2. `jump_hop_work_profile`; +3. `jump_hop_runtime_run`; +4. `jump_hop_event`; +5. `jump_hop_leaderboard_entry`,首版可暂不对外展示; +6. `jump_hop_gallery_view`; +7. `jump_hop_gallery_card_view`。 + +表结构新增字段必须按 SpacetimeDB 迁移规则放在结构体末尾并设置明确默认值。新增或调整表、reducer、procedure、view 后必须同步 `migration.rs`、表目录、生成 bindings,并执行 `npm run check:spacetime-schema`。 + +公开列表主路径应优先订阅 `jump_hop_gallery_card_view` 后在 `api-server` 本地 cache 构造列表响应,不要让每个 HTTP 请求都调用 SpacetimeDB procedure 组装全量列表。 + +## 10. 结果页能力 + +结果页必须展示: + +1. 作品标题; +2. 作品简介; +3. 角色形象; +4. 地块图集; +5. 路径预览; +6. 标签; +7. 试玩; +8. 发布; +9. 返回编辑。 + +结果页还必须支持: + +1. 单独重生成角色; +2. 单独重生成地块图集; +3. 单独修改标题和简介; +4. 单独调整标签和难度。 + +结果页不应强制再走一次封面生图。封面只做合成,不新增图像生成调用。 + +## 11. 运行态规则 + +运行态采用 2D 表现,但画面视觉上必须保留参考图那种俯视角 / 等距感。 + +### 11.1 核心玩法 + +1. 玩家长按蓄力; +2. 松手后角色按蓄力长度起跳; +3. 跳跃距离决定是否落到下一个地块; +4. 落在目标区域内判定成功; +5. 落在地块外或越界判定失败; +6. 到达终点地块判定通关。 + +### 11.2 判定规则 + +1. 只做一个当前局面的起跳判定; +2. 不做复杂连招动作树; +3. 不新增生命数、体力、回合数; +4. 不新增计时赛作为首版核心规则; +5. 不把前端动画结果当成最终真相,通关与失败必须能回写运行态状态。 + +### 11.3 角色动画 + +角色不需要多帧生图,运行态只通过位移、缩放、轻微旋转和投影变化表达: + +1. 蓄力时轻微压缩; +2. 起跳时向上抬升; +3. 空中保持可读轮廓; +4. 落地时轻微弹性回弹; +5. 失败时从地块边缘跌落。 + +### 11.4 摄像机与构图 + +1. 相机以当前角色和下一地块为中心; +2. 至少保证下一个落点一直可见; +3. 画面要留出顶部和底部的 UI 安全区; +4. 不要把地块做得太满,保留参考图那种疏朗感。 + +### 11.5 UI + +运行态 UI 只保留必要元素: + +1. 分数; +2. 暂停; +3. 重新开始; +4. 分享; +5. 结算按钮。 + +不默认展示大段规则说明。首进如果需要引导,只能用一次轻量提示,不允许常驻一屏的说明文案。 + +## 12. 视觉规范 + +本模板的视觉目标是“像 3D,但仍是 2D 图片”。 + +必须遵守: + +1. 平台有明确厚度; +2. 侧面可见分层或材质变化; +3. 投影统一且方向一致; +4. 背景干净,颜色克制; +5. 角色尺寸在小屏上依然可读; +6. 地块不能出现过多文字、按钮或装饰信息; +7. 不能把运行态做成重 UI 面板。 + +建议的背景策略: + +1. 以静态浅色渐变或纯色背景为主; +2. 不把背景也做成每次都生成的重资产; +3. 让地块和角色成为画面的第一视觉焦点。 + +## 13. 发布后体验 + +发布后的作品必须支持: + +1. 进入作品架和公开展示; +2. 分享; +3. 试玩; +4. 重新进入结果页编辑。 + +发布后的卡片封面应优先由角色图和地块图合成,不要求单独再生成封面图。 + +首版不新增排行榜、回放和对局对抗。后续如要扩展排行,可另起版本,不要塞进首版模板范围。 + +## 14. 验收 + +1. 创作入口能看到 `跳一跳` 模板; +2. 创作者可以填写主题、角色描述、风格和难度; +3. 提交后只生成一次角色图和一次地块图集; +4. 结果页能看到角色图、地块图集和路径预览; +5. 结果页可单独重生成角色或地块; +6. 试玩进入跳一跳运行态; +7. 长按蓄力、松手起跳、落点判定、失败和通关都可用; +8. 作品可以保存、发布和分享; +9. 前端不直接读取或暴露生图密钥; +10. 发布后的封面不依赖第三次额外生图。 +11. `npm run check:spacetime-schema` 在 schema 变更后通过; +12. `npm run check:encoding` 通过。 diff --git a/docs/technical/BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md b/docs/technical/BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md index 36aa7c74..0a3f9fbf 100644 --- a/docs/technical/BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md +++ b/docs/technical/BABY_OBJECT_MATCH_CREATION_PUBLISH_IMPLEMENTATION_2026-05-11.md @@ -1,4 +1,4 @@ -# 宝贝识物创作发布实现方案 2026-05-11 +# 宝贝识物创作发布实现方案 2026-05-11 ## 1. 范围 @@ -144,7 +144,7 @@ PUT /api/creation/edutainment/baby-object-match/drafts/{draftId} POST /api/creation/edutainment/baby-object-match/drafts/{draftId}/publish ``` -图片生成必须在后端调用 VectorEngine `gpt-image-2-all`,不得从前端直接调用外部图片接口。 +图片生成必须在后端调用 VectorEngine `gpt-image-2`,不得从前端直接调用外部图片接口。 后端 `2x2` 素材 sheet prompt 约束: diff --git a/docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md b/docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md index 697b7a8f..7dc401b6 100644 --- a/docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md +++ b/docs/technical/CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md @@ -1,4 +1,4 @@ -# 儿童动作识别互动玩法 Demo 热身关开发规格文档 +# 儿童动作识别互动玩法 Demo 热身关开发规格文档 > 日期:2026-05-09 > 关联设计文档:[CHILD_MOTION_DEMO_WARMUP_LEVEL_DESIGN_2026-05-09.md](../design/CHILD_MOTION_DEMO_WARMUP_LEVEL_DESIGN_2026-05-09.md) @@ -684,7 +684,7 @@ 1. 舞台主环境采用卡通绘本风格、明亮草地、天空、小山坡和树木的组合,默认背景环境需要保证中心与下方前景留空,便于角色轮廓和地面指示环叠加。 2. 该卡通绘本草地风格是儿童动作 Demo 后续场景、物品、UI 资源的全局风格要求;新增资源不得切回暗色科技风、真实照片风或后台面板风。 3. `src/index.css` 中的热身舞台、摄像头背景层、地面、角色轮廓、地面圆环、开始按钮和横屏提示均按绘本草地风格接入真实资源;资源加载失败时保留 CSS 兜底。 -4. 生成脚本固定为 `scripts/generate-child-motion-demo-assets.mjs`,并通过 `npm run assets:child-motion-demo` 触发;脚本使用 `gpt-image-2-all` 调用 VectorEngine `POST /v1/images/generations`,透明资源先生成品红底源图,再在本地移除色键,源图写入 `tmp/child-motion-demo-assets/`。 +4. 生成脚本固定为 `scripts/generate-child-motion-demo-assets.mjs`,并通过 `npm run assets:child-motion-demo` 触发;脚本使用 `gpt-image-2` 调用 VectorEngine `POST /v1/images/generations`,透明资源先生成品红底源图,再在本地移除色键,源图写入 `tmp/child-motion-demo-assets/`。 5. 当前已生成并接入以下正式 Demo 资源: - `public/child-motion-demo/picture-book-grass-stage.png`:默认草地舞台背景。 - `public/child-motion-demo/picture-book-foreground-grass-v2.png`:底部前景草坪条,只覆盖舞台下沿,不作为整块地板拉伸。 diff --git a/docs/technical/【后端架构】api-server能力模块化与生成资产Adapter总纲-2026-05-14.md b/docs/technical/【后端架构】api-server能力模块化与生成资产Adapter总纲-2026-05-14.md index 99a81aa0..6ace57c1 100644 --- a/docs/technical/【后端架构】api-server能力模块化与生成资产Adapter总纲-2026-05-14.md +++ b/docs/technical/【后端架构】api-server能力模块化与生成资产Adapter总纲-2026-05-14.md @@ -214,7 +214,7 @@ Handler 主要在 `story.rs`、`combat.rs`、`runtime_inventory.rs`: | Big Fish 正式图 | DashScope `wan2.2-t2i-flash` | 轮询 task 后 HTTP GET 图片 URL | `LegacyAssetPrefix::BigFishAssets` | 由 assetKind 映射主图/动作图/舞台背景等 | `big_fish_session` + session/entity id + slot | `big_fish.rs` 调用方 `execute_billable_asset_operation` | 配置缺失/上游失败直接错误;gallery 对部分 Spacetime 运行错误软降级 | | Square Hole 图片重生成 | OpenAI/VectorEngine GPT image helper | URL 下载或 base64/data URL 解码 | `LegacyAssetPrefix::SquareHoleAssets` | 方洞作品图片槽位相关 kind | profile/work + image slot | 调用方包裹 | 生成成功但入库失败保留 Data URL 回包 | | Custom World 场景/封面 | VectorEngine GPT image 2 / OpenAI helper | URL 下载或 base64 解码 | `LegacyAssetPrefix::CustomWorldScenes` 等 | scene/cover/opening storyboard | `custom_world_profile` 或 profile/landmark/scene slot | `custom_world_ai.rs` 调用方包裹 | entity/scene 生成存在 LLM fallback;资产持久化失败按当前错误口径返回 | -| Puzzle 图片 | GPT image 2 generations/edits | multipart/base64/URL 结果归一 | `LegacyAssetPrefix::PuzzleAssets` | puzzle level/background/generated image,另有 `puzzle_background_music` | puzzle profile/run/level slot | `puzzle.rs` 调用方包裹 | connectivity 可按既有规则跳过部分计费;运行态 fallback 保持原逻辑 | +| Puzzle 图片 | GPT image 2 generations/edits | 无参考图 JSON 创建;有参考图 multipart 编辑;base64/URL 结果归一 | `LegacyAssetPrefix::PuzzleAssets` | puzzle level/background/generated image,另有 `puzzle_background_music` | puzzle profile/run/level slot | `puzzle.rs` 调用方包裹 | connectivity 可按既有规则跳过部分计费;运行态 fallback 保持原逻辑 | | Match3D 图片 | APIMart/VectorEngine/OpenAI image helper | 下载、切图、透明化、校准后入库 | `LegacyAssetPrefix::Match3DAssets` | cover/background/item material sheet,音频 kind 另列 | match3d profile/session slot | `match3d.rs` 调用方包裹 | 新草稿不回退 Rodin/GLB;部分连接错误按现有计费跳过规则处理 | | Visual Novel 音频 | VectorEngine Suno/Vidu | 任务提交后按 task publish 下载音频 | 视觉小说/creation audio scope | `visual_novel_music`、`visual_novel_ambient_sound` | `visual_novel_scene` + scene id + `music`/`ambient_sound` | `vector_engine_audio_generation.rs` 调用方包裹 | 上游/下载失败显式错误,不混入图片 Adapter | | 通用音频 | VectorEngine Suno/Vidu | 同上 | creation audio scope | background_music/sound_effect 由调用方目标指定 | creation target entity/slot | 调用方包裹 | 不与 VN 场景语义混用 | diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index d6a26702..afef15c4 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -77,13 +77,16 @@ npm run check:server-rs-ddd 1. 每个能力 Module 只暴露 `router(state) -> Router`,由 `app.rs` 统一 `.merge(...)`。 2. `app.rs` 只保留全局 middleware、TraceLayer、request context、tracking middleware、入口开关和少量顶层 glue。 -3. 路由迁移和业务重构分阶段处理;先移动路由装配,再拆 handler 内部实现。 -4. 大 handler 拆分时优先按 `router.rs`、`handlers.rs`、`application.rs`、`assets.rs`、`mapper.rs`、`errors.rs` 分层。`handlers.rs` 只做 Axum extract、鉴权和 request/response,业务规则继续下沉到 `module-*`。 -5. 手写 Rust 模块入口统一使用同名 `.rs` 文件,例如 `puzzle.rs` + `puzzle/*.rs`、`match3d.rs` + `match3d/*.rs`;不要再新增 `mod.rs` 入口。生成的 SpacetimeDB Rust bindings 也由生成脚本同步为 `module_bindings.rs` + `module_bindings/*.rs` 布局。 +3. 能力 Module 可在路由内部用 `FromRef` 派生自己的 Feature State,例如 `PuzzleApiState`。全局 `AppState` 仍作为进程组合根、鉴权层和全局中间件状态,但业务 handler 优先只提取对应 Feature State,不直接暴露完整 `AppState`。 +4. Feature State 只暴露该能力实际需要的 facade / adapter / 配置快照;若必须复用仍要求 `AppState` 的横切 helper(例如计费、外部失败审计或通用 tracking),应通过 Feature State 的窄方法或显式 `root_state()` 过渡,并在后续继续收窄。 +5. 路由迁移和业务重构分阶段处理;先移动路由装配,再拆 handler 内部实现,再收窄 handler 可见状态。 +6. 大 handler 拆分时优先按 `router.rs`、`handlers.rs`、`application.rs`、`assets.rs`、`mapper.rs`、`errors.rs` 分层。`handlers.rs` 只做 Axum extract、鉴权和 request/response,业务规则继续下沉到 `module-*`。 +7. 手写 Rust 模块入口统一使用同名 `.rs` 文件,例如 `puzzle.rs` + `puzzle/*.rs`、`match3d.rs` + `match3d/*.rs`;不要再新增 `mod.rs` 入口。生成的 SpacetimeDB Rust bindings 也由生成脚本同步为 `module_bindings.rs` + `module_bindings/*.rs` 布局。 拼图 `api-server` 内部拆分: - `server-rs/crates/api-server/src/modules/puzzle.rs` 只负责路由装配、鉴权层和参考图 body limit;对外继续引用同一批 handler 名称。 +- `server-rs/crates/api-server/src/state.rs` 中的 `PuzzleApiState` 是拼图 HTTP/BFF 的 Feature State,集中暴露 `SpacetimeClient`、`PuzzleGalleryCache`、OSS client、作者查询所需认证服务、拼图 LLM client 和少量 VectorEngine / Agent 配置快照。拼图 handler 只提取 `State`,不得重新改回 `State`。 - `server-rs/crates/api-server/src/puzzle.rs` 只作为聚合入口,保留共享 import / 常量、内部模块声明和 handler re-export,不继续承载大段实现。 - `server-rs/crates/api-server/src/puzzle/handlers.rs` 承接 Axum handler,负责 extract、鉴权上下文、调用 SpacetimeDB facade / 编排 helper,并返回 HTTP/SSE 响应。 - `server-rs/crates/api-server/src/puzzle/draft.rs` 承接表单草稿保存、草稿编译、首关命名、UI 背景 prompt、降级 snapshot 和初始资产就绪校验。 @@ -101,8 +104,8 @@ npm run check:server-rs-ddd - `server-rs/crates/api-server/src/match3d/handlers.rs` 承接 Axum handler,负责 extract、鉴权上下文、调用 SpacetimeDB facade / 编排 helper,并返回 HTTP 响应。 - `server-rs/crates/api-server/src/match3d/draft.rs` 承接 Agent session、草稿编译、题材 / 难度 / 物品计划和草稿持久化编排。 - `server-rs/crates/api-server/src/match3d/works.rs` 承接作品 CRUD、封面 / 背景 / 容器资产生成入口、发布 / Remix / 点赞 / 游玩记录和作品级 helper。 -- `server-rs/crates/api-server/src/match3d/item_assets.rs` 承接物品 sheet 生成、绿幕 / 近白底透明化、切图、append / replace / delete / sort / merge 和素材持久化。 -- `server-rs/crates/api-server/src/match3d/vector_engine_gemini.rs` 承接 VectorEngine Gemini 请求体、响应解析、base64 图片下载和上游错误归一。 +- `server-rs/crates/api-server/src/match3d/item_assets.rs` 承接物品生成批次编排、append / replace / delete / sort / merge、计费外层和草稿素材映射;sheet prompt、绿幕 / 近白底透明化、切图和切片持久化复用 `generated_asset_sheets` 通用模块。 +- `server-rs/crates/api-server/src/match3d/vector_engine_gemini.rs` 仅保留历史 VectorEngine Gemini 物品 sheet helper;当前草稿物品 spritesheet 以关卡整图为参考走 `gpt-image-2` 编辑链路,提示词、绿幕透明化和 OSS 持久化由 `item_assets.rs` / `works.rs` 约束。 - `server-rs/crates/api-server/src/match3d/runtime.rs` 保留运行态轻量归一 helper;`mappers.rs` / `tags.rs` / `tests.rs` 分别承接 DTO 映射、标签 / 通用错误 helper 和原有单测。 该拆分只改变 `api-server` 文件组织,不改变 `/api/creation/match3d/*`、`/api/runtime/match3d/*` route、DTO、error envelope、SpacetimeDB schema、公开 gallery cache 语义、VectorEngine / OSS 副作用边界或计费语义;后续继续细分时也必须先保持行为不变,再单独讨论领域规则下沉到 `module-match3d`。 @@ -114,6 +117,8 @@ npm run check:server-rs-ddd 3. Adapter 输出应保留 legacy public path、object key、asset object id、MIME、extension、task id 和实际 prompt。 4. Adapter 不负责扣费、退款或钱包读取;计费仍由调用方显式包裹。 5. Puzzle、Match3D、音频、GLB、视频等复杂媒体可以复用 OSS + asset object + binding 的底层持久化能力,但玩法专属处理规则留在各自编排层,不塞进公共接口。 +6. 拼图图生图参考图主链不得再把大图 Data URL 塞进创作 JSON body;前端先直传 OSS 并提交 `referenceImageAssetObjectId(s)`,`api-server` 校验 `asset_object` 的 bucket、kind、图片 MIME、大小和 owner 后签发只读 URL 给 VectorEngine 读取,Data URL / `/generated-*` 仅作为旧请求兼容。 +7. 系列素材图集使用 `server-rs/crates/api-server/src/generated_asset_sheets.rs`:调用方必须传入 `grid_size` 作为 `n*n` 的 `n`,可选传入物品名称 prompt 模板和特殊设定 prompt;模块负责 sheet prompt 组装、按 `n*n` 切片、透明化、PNG 输出、OSS private upload 请求构造和 sheet / item / special prompt 元数据持久化。玩法只负责规划 slot、调用具体生图 provider、计费、失败回写,以及把通用切片结果映射回自己的 DTO / 草稿 / runtime 字段。 ## SpacetimeDB schema 变更规则 @@ -151,12 +156,12 @@ npm run check:server-rs-ddd - LLM:`GENARRATIVE_LLM_*`,创意 Agent 另用 `APIMART_BASE_URL` / `APIMART_API_KEY`。 - 图片生成:VectorEngine / APIMart / DashScope,密钥只在后端环境变量中。 -- Match3D 物品 sheet:VectorEngine Gemini `gemini-3-pro-image-preview` 原生 `generateContent`。 -- Match3D 封面和 9:16 纯背景:VectorEngine `/v1/images/generations`。 -- Match3D 1:1 容器 UI:VectorEngine `/v1/images/edits` multipart 参考图。 +- Match3D 物品 sheet:关卡整图完成后走 VectorEngine `/v1/images/edits` multipart `image`,模型为 `gpt-image-2`,`2K 1:1` 输出 `10*10` spritesheet;物品 sheet prompt 固定要求纯绿色绿幕背景,后端上传 OSS 前必须把绿幕扣成透明 PNG,并把透明整图写入 `itemSpritesheetImageSrc/itemSpritesheetImageObjectKey`。后端固定从该 sheet 解析并持久化 20 个物品、每个 5 个形态;通用系列素材图集的行列索引按每行 2 个物品计算,必须落在 `1..=10`,难度只决定运行态加载 3 / 9 / 15 / 20 种。 +- Match3D UI spritesheet 和背景派生图:关卡整图作为参考图并发生成 `1K 1:1` UI spritesheet 与 `1K 9:16` 背景图,模型均为 `gpt-image-2`。UI spritesheet prompt 固定要求纯绿色绿幕背景,后端上传 OSS 前必须把绿幕扣成透明 PNG;背景图必须合成为全画幅不透明 PNG。 - Hyper3D / Rodin:只保留后端安全代理和旧数据兼容;新 Match3D 草稿和批量新增不再生成 GLB。 - 音频:视觉小说专用音频路由保留;拼图和抓大鹅生成入口暂时关闭,通用 `/api/creation/audio/*` 对相关目标返回 `410 Gone`。 - OSS:私有 generated legacy path 进入浏览器前必须通过 `/api/assets/read-url` 换签;不要裸请求 `/generated-*`。 +- 外部 API 失败审计:外部供应商调用未成功时,`api-server` 必须发送 OTLP 失败事件并写入 `tracking_event`。当前通用 VectorEngine `gpt-image-2-all` 图片生成 / 编辑适配器在 `request_send`、`response_body`、`upstream_status`、`response_parse`、`missing_image` 和 `image_download` 阶段失败时记录 `external_api_call_failure`,`scope_kind = module`、`scope_id = provider`、`module_key = external-api`;metadata 固定包含 provider、endpoint、operation、failureStage、statusCode、statusClass、timeout、retryable、errorMessage、latencyMs、promptChars、referenceImageCount 和 imageModel。入库优先复用 tracking outbox,outbox 不可写或保护阈值拒绝时回退同步写 SpacetimeDB;不得新增前端兜底或在 SpacetimeDB reducer 内做外部 I/O。 ## SpacetimeDB 表目录 @@ -324,6 +329,7 @@ npm run check:server-rs-ddd - Rust 结构体:`CustomWorldAgentSession` - 源码:`server-rs/crates/spacetime-module/src/custom_world.rs` +- 发布约束:`publish_world` 的 action payload 不要求携带 `settingText`;`spacetime-module` 调用 `module-custom-world::resolve_custom_world_publish_setting_text(...)`,优先从当前 `draft_profile_json` 草稿真相派生正式 `setting_text`,避免旧会话 `seed_text` 为空时在最终 compile / publish 阶段触发 `custom_world.setting_text 不能为空`。 ### `custom_world_draft_card` @@ -361,6 +367,40 @@ npm run check:server-rs-ddd - Rust 结构体:`InventorySlot` - 源码:`server-rs/crates/spacetime-module/src/gameplay.rs` +### `jump_hop_agent_session` + +- Rust 结构体:`JumpHopAgentSessionRow` +- 源码:`server-rs/crates/spacetime-module/src/jump_hop/tables.rs` + +### `jump_hop_event` + +- Rust 结构体:`JumpHopEventRow` +- 源码:`server-rs/crates/spacetime-module/src/jump_hop/tables.rs` + +### `jump_hop_runtime_run` + +- Rust 结构体:`JumpHopRuntimeRunRow` +- 源码:`server-rs/crates/spacetime-module/src/jump_hop/tables.rs` + +### `jump_hop_work_profile` + +- Rust 结构体:`JumpHopWorkProfileRow` +- 源码:`server-rs/crates/spacetime-module/src/jump_hop/tables.rs` + +### SpacetimeDB view:`jump_hop_gallery_card_view` + +- Rust view:`jump_hop_gallery_card_view` +- 返回类型:`Vec` +- 源码:`server-rs/crates/spacetime-module/src/jump_hop.rs` +- 说明:跳一跳公开广场列表卡片投影,只暴露 `publication_status = Published` 的作品卡片字段;`api-server` 的 `spacetime-client` 长期订阅 `SELECT * FROM jump_hop_gallery_card_view` 后,从本地 cache 构造跳一跳公开列表响应。个人作品列表、详情、发布和运行态仍按 procedure 路径处理。 + +### SpacetimeDB view:`jump_hop_gallery_view` + +- Rust view:`jump_hop_gallery_view` +- 返回类型:`Vec` +- 源码:`server-rs/crates/spacetime-module/src/jump_hop.rs` +- 说明:跳一跳公开详情兼容投影,包含作品、路径和素材字段;公开列表主路径优先使用 `jump_hop_gallery_card_view`。 + ### `match3d_agent_message` - Rust 结构体:`Match3DAgentMessageRow` @@ -545,6 +585,7 @@ npm run check:server-rs-ddd - `SELECT * FROM square_hole_gallery_view` - `SELECT * FROM visual_novel_gallery_view` - `SELECT * FROM big_fish_gallery_view` +- `SELECT * FROM jump_hop_gallery_card_view` 下列订阅用于统计或配置缓存,订阅失败不会让公开列表连接整体不可用,调用方保留兼容兜底: @@ -561,6 +602,10 @@ npm run check:server-rs-ddd `GET /api/creation-entry/config` 和入口熔断优先从订阅 cache 读取创作入口配置;cache 缺失时使用最近一次成功读取的内存快照,再兜底调用 `get_creation_entry_config` procedure 完成空库种子或旧库兼容。 +RPG 创作入口的配置 ID 是 `rpg`,当前 `visible=true`、`open=true`;历史 `custom-world` 路由仍是 RPG 的工程域与运行态源类型。入口熔断把 `/api/runtime/custom-world*`、`/api/story/*` 和 `/api/runtime/chat/*` 统一映射到 `rpg`,不要新增平行 `airp` 路由或用 `airp` 接管当前文字冒险链路。 + +结构化创作和 RPG 的 LLM JSON 链路默认不启用 Responses `web_search`;只有在明确需要联网增强时,才通过 `GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED` 或 `GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED` 显式打开。否则未开通工具的上游会先吐自然语言再返回 `ToolNotOpen`,这类失败要按上游工具不可用处理,不要误判成模型返回结果解析失败。 + 未来可选:若发现页、推荐流和各玩法广场需要统一给浏览器前端直接订阅公开作品列表,只新增 / 统一专用 public read model,例如 `public_work_gallery_entry`。该 read model 必须是后端投影后的公开作品卡片契约,覆盖作品类型、公开作品号、标题、摘要、封面、作者展示名、排序键、公开统计和入口开关后的可见性,不暴露玩法领域源表 row shape。前端可选择订阅这个稳定投影来减少 HTTP 拉取,但不能订阅 `puzzle_work_profile`、`custom_world_profile` 等源表后自行拼装列表;BFF 仍保留首屏、SEO / 分享、旧客户端、订阅失败和灰度期间的 HTTP fallback。 ### `quest_log` @@ -636,6 +681,7 @@ npm run check:server-rs-ddd - Rust 结构体:`TrackingEvent` - 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs` - 写入:关键业务埋点同步调用单条 procedure;普通 HTTP route tracking 由 `api-server` 本机 outbox 批量调用 `record_tracking_events_and_return`。outbox 到达批量阈值时先封存 active 文件并切新 active,后台 worker 异步 flush sealed 文件,HTTP 请求线程不等待 SpacetimeDB。`FLUSH_INTERVAL_MS` 只负责兜底封存长时间未满批的 active 文件,`MAX_BYTES` 只做磁盘保护阈值。`event_id` 必须稳定且全局唯一,批量重试时用唯一索引做幂等跳过。 +- 外部 API 失败:`event_key = external_api_call_failure` 使用同一张表落库;它是供应商失败审计事实,不新增 SpacetimeDB 表,查询时按 `module_key = 'external-api'` 或 `scope_kind = module AND scope_id = ''` 过滤。 ### `treasure_record` diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 8bc5ec79..c0dd000f 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -40,10 +40,12 @@ npm run dev:web 单独启动 Rust API server: ```bash -npm run api-server +npm run dev:api-server ``` -后端日志默认写入 `logs/api-server/`。后端 API smoke 使用 `npm run api-server` 并检查 `/healthz`;不要使用旧 `api-server:maincloud` 或任何 `GENARRATIVE_SPACETIME_MAINCLOUD_*` 口径。 +后端日志默认写入 `logs/api-server/`。后端 API smoke 使用 `npm run dev:api-server` 并检查 `/healthz`;不要使用旧 `api-server:maincloud` 或任何 `GENARRATIVE_SPACETIME_MAINCLOUD_*` 口径。 + +本地 `.env`、`.env.local` 或 `.env.secrets.local` 修改后必须重启 `api-server` 才会生效;若已经通过 `npm run dev` 启动完整联调,可在该终端输入 `rs api-server`。排查 RPG / 拼图 / 抓大鹅等 VectorEngine 生图链路时,确认 `VECTOR_ENGINE_BASE_URL`、`VECTOR_ENGINE_API_KEY` 和 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 只在本地或服务器密钥文件中配置,不能写入 Git。开局 CG 故事板、首图、背景和图集都属于长耗时图片请求;后端默认会把 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 下限收口到 `1000000`,旧进程仍可能沿用重启前的短超时。若开局 CG 故事板在 `send()` 阶段失败且日志显示 `SendRequest`,先看同一 request_id 的 `request_body_bytes`、`reference_data_url_bytes`、`sourceChain` 和 `rootSource`;当前开局 CG 会把角色图与首幕背景图压到单边 768 的 JPEG 后再作为 generations `image` 数组发送,`/v1/images/generations` 使用默认 HTTP 协商,只有 multipart `/v1/images/edits` 单独强制 HTTP/1.1。 查看本地 Rust / SpacetimeDB 日志: @@ -94,6 +96,33 @@ SpacetimeDB bindings: npm run spacetime:generate ``` +## CodeGraph 本地代码索引 + +项目已安装 `@colbymchenry/codegraph` 作为开发期依赖,用于在本地生成语义代码索引,辅助 AI / IDE 做符号搜索、调用关系和影响范围分析。索引目录为 `.codegraph/`,其中 `config.json` 可提交,数据库、缓存和日志由 `.codegraph/.gitignore` 保持本机私有。 + +首次拉取或需要重建索引时: + +```bash +npm install +npm run codegraph:init +``` + +日常使用: + +```bash +npm run codegraph:status +npm run codegraph:sync +npm run codegraph:index +``` + +Codex 项目级 hook 已放在 `.codex/config.toml` 与 `.codex/hooks/`: + +- `PreToolUse` hook 会在 Codex 准备执行 `git commit` 前运行 `node .codex/hooks/pre-submit-compile-check.mjs`,依次执行 `npm run typecheck`、`npm run admin-web:typecheck`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`,发现编译错误会阻止本次提交。 +- `PostToolUse` hook 会在 Codex 工具修改文件后运行 `node .codex/hooks/post-edit-codegraph-sync.mjs`,执行 `npm run codegraph:sync` 刷新本地语义索引。 +- 如果某个 Codex 客户端版本尚未自动加载项目级 hook,可先手动运行 `node .codex/hooks/pre-submit-compile-check.mjs` 与 `node .codex/hooks/post-edit-codegraph-sync.mjs`;个人模型、token、MCP server 仍放在个人 `~/.codex/config.toml`,不要提交。 + +若要把 CodeGraph 接到 Codex CLI / Cursor / Claude Code 等 MCP 客户端,按本机 agent 配置执行 `codegraph install` 或参考 `codegraph install --print-config codex` 输出;不要把个人全局 agent 配置、token 或本机绝对路径提交到仓库。 + ## 后端改动验收 后端代码修改后,按变更范围选择: @@ -103,7 +132,7 @@ npm run spacetime:generate - `cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml` - `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml` - `npm run check:server-rs-ddd` -- `npm run api-server` 后请求 `/healthz` +- `npm run dev:api-server` 后请求 `/healthz` 涉及 SpacetimeDB schema 时必须补: @@ -153,16 +182,18 @@ Windows Stdb module 构建流水线运行在 Jenkins `windows` 节点上。该 生产环境变量模板:`deploy/env/api-server.env.example`。真实密钥只放服务器,不提交 Git,不写入文档示例。 -`Genarrative-Server-Provision` 会安装基础构建依赖、systemd 模板和 Nginx 站点模板。Ubuntu / apt 目标机会额外安装 `libnginx-mod-http-brotli-filter` 与 `libnginx-mod-http-brotli-static`,随后由 `scripts/jenkins-server-provision.sh` 通过临时 `nginx -t` 配置探测 Brotli 指令是否可用;该临时配置必须先 `include /etc/nginx/modules-enabled/*.conf`,因为 apt 安装的 Brotli 是动态模块,不会出现在普通 `nginx -V` 编译参数里。探测成功才在渲染后的 `deploy/nginx/genarrative.conf` / `genarrative-dev-http.conf` 中启用 Brotli,避免未安装模块的机器直接写入无效配置。 +`Genarrative-Server-Provision` 会安装基础构建依赖、systemd 模板和 Nginx 站点模板。Ubuntu / apt 目标机会额外安装 `libnginx-mod-http-brotli-filter` 与 `libnginx-mod-http-brotli-static`,随后由 `scripts/jenkins-server-provision.sh` 通过临时 `nginx -t` 配置探测 Brotli 指令是否可用;该临时配置必须先 `include /etc/nginx/modules-enabled/*.conf`,因为 apt 安装的 Brotli 是动态模块,不会出现在普通 `nginx -V` 编译参数里。探测成功才在渲染后的 `deploy/nginx/genarrative.conf` / `genarrative-dev-http.conf` 中启用 Brotli,避免未安装模块的机器直接写入无效配置。Provision 写入 Genarrative Nginx 站点时会把 `/etc/nginx/sites-enabled/default*` 移到 `/etc/nginx/sites-disabled/`,避免 Debian / Certbot 默认站点继续占用 `genarrative.world` / `www.genarrative.world` 并在 `nginx -T` 中出现 `conflicting server name ... ignored`。如果 `nginx -t` 失败,脚本会恢复写入前的 Genarrative 配置和被移动的默认站点。 50 HTTP req/s 首版压测优化口径: - `api-server` 生产模板默认 `GENARRATIVE_API_LISTEN_BACKLOG=1024`、`GENARRATIVE_API_WORKER_THREADS=4`;本地未设置 worker threads 时继续使用 Tokio 默认值。 - `GENARRATIVE_API_MAX_CONCURRENT_REQUESTS=512` 开启应用内 HTTP 并发背压;`GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS=320`、`GENARRATIVE_API_DETAIL_MAX_CONCURRENT_REQUESTS=64`、`GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS=16` 分别限制公开列表、公开详情和后台 API 热路径。超过许可时直接返回 `429 Too Many Requests` 和 `Retry-After: 1`,`/healthz` 不受该限制。这些值不是 RPS 限速;如果压测中 429 上升但内存和 p95 收敛,说明背压正在保护进程。直连 `api-server` 的极高 RPS 压测若出现 `connection refused`,通常已经打到 TCP 监听 / accept 层,应同时检查 backlog、Nginx upstream keepalive 和前置限流。 - `genarrative-api.service` 设置 `LimitNOFILE=65535`、`TasksMax=2048`;上线后用 `systemctl show genarrative-api.service -p LimitNOFILE -p TasksMax` 和 `cat /proc/$(pidof api-server)/limits` 核对。 -- Server provision 不在目标机下载 SpacetimeDB 或 `otelcol-contrib`。Jenkins 的 `Prepare Provision Tools` 阶段在 `linux && genarrative-build` 构建机执行 `scripts/prepare-server-provision-tools.sh`:SpacetimeDB 仍通过官方安装入口 `https://install.spacetimedb.com` 准备;`otelcol-contrib` 默认要求在 Jenkins 参数 `OTELCOL_CONTRIB_ARCHIVE` 手动上传 `otelcol-contrib_0.151.0_linux_amd64.tar.gz`,再从上传包解出 `provision-tools/otelcol-contrib`。最终工具包通过 `stash/unstash` 上传到 release 部署 agent。目标机上的 `scripts/jenkins-server-provision.sh` 只从该工作区工具包安装 `/stdb/spacetime`、`/stdb/bin/current/*` 和 `/usr/local/bin/otelcol-contrib`。注意 `scripts/jenkins-checkout-source.sh` 会执行 `git reset --hard` / `git clean`,因此被直接执行的新增脚本必须以 Git `100755` 模式提交,或在二次 checkout 之后再补 `chmod +x`。 +- Server provision 不在目标机联网下载 SpacetimeDB 或 `otelcol-contrib`。`Genarrative-Server-Provision` 先在 Windows Jenkins 节点执行 `Download Provision Tool Archives`,把 `spacetime-x86_64-unknown-linux-gnu.tar.gz` 和 `otelcol-contrib_0.151.0_linux_amd64.tar.gz` 先下载到工作区,再通过 `stash/unstash` 带到 `genarrative-build-01`;Windows 下载前会先查 GitHub release asset 的 `digest` 字段做 SHA256 校验,已有本地文件且 digest 一致就直接复用,不再重复下载。目标 Linux 节点上的 `scripts/prepare-server-provision-tools.sh` 只消费这些本地下载件生成 `provision-tools/`,再交给 `scripts/jenkins-server-provision.sh` 安装 `/stdb/spacetime`、`/stdb/bin/current/*` 和 `/usr/local/bin/otelcol-contrib`。Windows 侧的 `runWindowsPowerShell(...)` 现在会先 `writeFile` 生成 UTF-8 `.ps1`,再直接把脚本文本读入内存并通过 `ScriptBlock::Create(...)` 执行,避免对同一个 workspace 脚本做原地 BOM 重写。排查时除了看下载日志,还要看 `[jenkins-powershell] workspace:`、`[jenkins-powershell] script:` 和 `[jenkins-powershell] loaded bytes:`。注意 `scripts/jenkins-checkout-source.sh` 会执行 `git reset --hard` / `git clean`,因此被直接执行的新增脚本必须以 Git `100755` 模式提交,或在二次 checkout 之后再补 `chmod +x`。 +- Windows 下载阶段如果出现 `curl: (18)` 或响应体截断,流水线会保留同名 `.download` 临时文件并用 `curl -C -` 断点续传;只有完整返回但 SHA256 digest 仍不匹配时才删除临时文件后重新下载。目标 Linux 节点仍只接收 `stash/unstash` 带过去的本地下载件,不回退外网下载。 +- Windows 下载阶段如果走代理,在 `Genarrative-Server-Provision` 参数 `PROVISION_DOWNLOAD_PROXY` 填写 Windows Jenkins 节点可访问的 HTTP 代理,例如 `http://127.0.0.1:7890`;不要填写目标 release 机器视角的 `127.0.0.1`,除非代理确实运行在该 Windows 节点本机。Linux 目标机阶段会强制要求使用本地下载件,缺少文件直接失败,不再回退到外网下载。 - `otelcol-contrib.service` 作为可选系统服务加入 provision,默认监听 `127.0.0.1:4317/4318` 并使用 `deploy/otelcol/genarrative-debug.yaml`。api-server 是否发送 OTLP 仍由 `GENARRATIVE_OTEL_ENABLED` 控制,服务 unit 见 `deploy/systemd/otelcol-contrib.service`。 -- Nginx `/api/` 与 `/admin/api/` 通过 `genarrative_api` upstream 代理到 `127.0.0.1:8082`,upstream keepalive 为 64;`limit_conn` 负责连接 / 并发保护,`limit_req` 负责入口 RPS 快拒绝。当前模板把公开 gallery list 单独放到 `genarrative_gallery_rps`,默认 `rate=5000r/s`、`burst=4096`、`limit_conn=320`;公开详情和普通 API 放到 `genarrative_api_rps`,后台 API 放到 `genarrative_admin_rps`。压测时看 `/var/log/nginx/genarrative.access.log` 中的 `request_time`、`upstream_connect_time`、`upstream_header_time`、`upstream_response_time`、`upstream_status`、`request_id`。 +- Nginx `/api/` 与 `/admin/api/` 通过 `genarrative_api` upstream 代理到 `127.0.0.1:8082`,upstream keepalive 为 64;`limit_conn` 负责连接 / 并发保护,`limit_req` 负责入口 RPS 快拒绝。当前模板把公开 gallery list 单独放到 `genarrative_gallery_rps`,默认 `rate=5000r/s`、`burst=4096`、`limit_conn=320`;公开详情和普通 API 放到 `genarrative_api_rps`,后台 API 放到 `genarrative_admin_rps`。通用 `/api` location 设置 `client_max_body_size 64m` 只是反代兜底,防止旧客户端或兼容请求在到达 `api-server` 前被默认 1 MiB 上限拦截;长期主链不得依赖大 JSON body 承载图片,拼图参考图应先直传 OSS,只向创作接口提交 `referenceImageAssetObjectId(s)`,由后端签只读 URL 给外部模型读取。真实业务上限仍由 Rust 路由 `DefaultBodyLimit`、资产确认时 OSS HEAD 和解码后字节校验控制。若线上出现 `413 Request Entity Too Large` 且 access log 中 `request_time=0.000`、`upstream_status=-`,说明请求在 Nginx 层被拦截,先用 `nginx -T | grep client_max_body_size` 检查 release 模板是否已渲染并 reload,同时检查前端是否仍在提交 Data URL 而不是 `assetObjectId`。`limit_conn_status 429` 和 `limit_req_status 429` 必须在 HTTP 与 HTTPS server 中同时生效;若线上压测看到 `limiting connections by zone "genarrative_api_conn"` 却返回 503,优先检查 `nginx -T` 里 HTTPS server 是否缺少这些状态码,以及 `/api/runtime/puzzle/gallery` 是否误落到通用 `location ~ ^/api` 的 `limit_conn=64`。压测时看 `/var/log/nginx/genarrative.access.log` 中的 `request_time`、`upstream_connect_time`、`upstream_header_time`、`upstream_response_time`、`upstream_status`、`request_id`。 - 作品列表 K6 脚本一次 iteration 默认请求两个公开接口,因此约 50 HTTP req/s 的目标命令使用 `SCENARIO=spike START_RPS=5 PEAK_RPS=25 HOLD=60s END_RPS=5 DETAIL_RATIO=0 npm run loadtest:k6:works`。 - 作品列表短期继续由 `api-server` / BFF 订阅 SpacetimeDB 公开 read model 后读本地 cache,不让浏览器前端直接订阅完整列表;未来如新增 `public_work_gallery_entry` 等专用公开作品列表 read model,前端只可订阅稳定、低基数、公开的专用投影,禁止订阅 `puzzle_work_profile`、`custom_world_profile` 等玩法源表后自行 join、聚合或判断权限。前端直订阅落地前必须先补齐权限、字段契约、排序 / 分页、埋点和 BFF 回退策略。 - 50 HTTP req/s 验收目标为 `http_req_failed < 1%`、`p95 < 2s`、`dropped_iterations = 0`,同时压测窗口内 Nginx 无新增 502。2026-05-19 容器 2C / 2G 连续 10 轮不重启 SpacetimeDB 压测:`PEAK_RPS=2500` 等价约 5000 HTTP req/s,平均实际吞吐约 `4219 HTTP req/s`,10 轮总计 `1,897,357` 个 200、`212,542` 个 429、`0` 个 5xx,200 请求平均 `p95=123ms`、`p99=234ms`;该档会把 SpacetimeDB 容器内存从约 `366MiB` 推到约 `885MiB / 896MiB`,因此当前不要继续抬公开 gallery 入口并发,应优先处理 SpacetimeDB 侧连接 / 订阅 / tracking 写入后的内存高水位。 @@ -178,7 +209,7 @@ npm run container:k6 npm run container:down ``` -容器方案默认暴露 `http://127.0.0.1:18080`,`api-server` 在容器内监听 `0.0.0.0:8082`,Nginx 通过 `api-server:8082` upstream 反代 `/api/` 和 `/admin/api/`。SpacetimeDB 也纳入 compose,容器内由 `spacetimedb:3101` 提供服务,宿主机通过 `http://127.0.0.1:13101` 进行模块发布;Collector 镜像使用 `otel/opentelemetry-collector-contrib:0.151.0`。生产 provision 侧则通过 Jenkins 构建机准备的 `provision-tools/otelcol-contrib` 安装本机 `otelcol-contrib.service`,真实库名、token 和外部服务密钥只写本地 `deploy/container/api-server.env`,不提交 Git。完整拓扑、端口、k6 参数和 OTLP debug exporter 使用方法见 `deploy/container/README.md`。 +容器方案默认暴露 `http://127.0.0.1:18080`,`api-server` 在容器内监听 `0.0.0.0:8082`,Nginx 通过 `api-server:8082` upstream 反代 `/api/` 和 `/admin/api/`。SpacetimeDB 也纳入 compose,容器内由 `spacetimedb:3101` 提供服务,宿主机通过 `http://127.0.0.1:13101` 进行模块发布;Collector 镜像使用 `otel/opentelemetry-collector-contrib:0.151.0`。生产 provision 侧则通过 Windows Jenkins 下载件在目标 Linux 节点生成 `provision-tools/otelcol-contrib`,再安装本机 `otelcol-contrib.service`,真实库名、token 和外部服务密钥只写本地 `deploy/container/api-server.env`,不提交 Git。完整拓扑、端口、k6 参数和 OTLP debug exporter 使用方法见 `deploy/container/README.md`。 `npm run container:config` 默认只做 quiet 校验,避免把本地 env 中的 token 展开到终端;确需排查完整 compose 时再传 `-- --print`。 OpenTelemetry 现阶段默认开启 OTLP traces / metrics / logs,但本地日志与 Nginx 文件日志仍保留: @@ -191,6 +222,7 @@ OpenTelemetry 现阶段默认开启 OTLP traces / metrics / logs,但本地日 - debug exporter / Rider 转发都会同时接收 traces、metrics 和 logs。 - api-server 会随 metrics 发送进程级指标:`process.memory.usage`、`process.memory.virtual`、`process.cpu.time`、`genarrative.process.cpu.usage_percent`、`process.thread.count`、`genarrative.process.memory.private`;Windows 额外发送 `process.windows.handle.count`,Linux 额外发送 `process.unix.file_descriptor.count`。这些指标只描述当前进程,不携带请求、用户或作品 label。 - HTTP 运行态补充发送 `genarrative.http.server.response_bodies.in_flight` 与 `genarrative.http.server.request_permits.available`,后者带低基数 `pool=default|gallery|detail|admin` label,用于区分业务 handler / 背压 permit 是否仍被占用;拼图广场热点缓存补充发送 `genarrative.puzzle_gallery.cache.*` 指标,记录 fresh hit、stale hit、未命中、后台刷新开始 / 失败、重建耗时和预序列化 data JSON 字节数。 +- 外部 API 失败统一发送 OTLP 并落库。当前 VectorEngine `gpt-image-2-all` 图片生成 / 编辑失败会输出 `外部 API 调用失败` trace/log,并记录指标 `genarrative.external_api.failures{provider,failure_stage,status_class,retryable}`;同时写入 `tracking_event`,`event_key = external_api_call_failure`、`module_key = external-api`、`scope_kind = module`、`scope_id = provider`。排障时先按 provider / failureStage 聚合,再结合 request 日志和上游响应 excerpt 判断是限流、超时、解析失败还是未返回图片。 - SpacetimeDB 观测分为两类:procedure / reducer 调用继续用 `genarrative.spacetime.procedure.*`,订阅本地 cache 读使用 `genarrative.spacetime.read.*`。`read=list_puzzle_gallery` 表示拼图广场当前从 `puzzle_gallery_card_view` 本地 cache 读取,不再每个 HTTP 请求调用 `list_puzzle_gallery` procedure。 - 本地 Windows 直连压测的内存高水位要结合 K6 VU / 连接数解释。250 RPS 下过高 `PREALLOCATED_VUS` 可能让 300 个本地 Established 连接把 `api-server` private memory 瞬时推到 GB 级,且 `/healthz` 小响应也能复现;若压测结束后回落、`response_bodies.in_flight` 和背压 permit 未显示业务积压,应优先按连接 / 发送链路高水位处理,而不是判断为 SpacetimeDB 或 JSON 缓存泄漏。 - Rider 的 Logs 面板只展示 log event 自身字段,不会自动展开父 span 的全部 attributes;请求完成日志会直接带 `request_id`、`http.request.method`、`http.route`、`url.scheme`、`url.path`、`http.response.status_code`、`status_class`、`latency_ms` 和 `slow_request`,完整链路继续到 Traces 面板按 trace/span 查看。 @@ -211,6 +243,8 @@ OpenTelemetry 现阶段默认开启 OTLP traces / metrics / logs,但本地日 - `WECHAT_*` - `ALIYUN_OSS_*` +结构化创作 / RPG 的 Responses JSON 链路默认不打开 `web_search`;本地和生产如需联网增强,必须显式配置 `GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED=true` 或 `GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED=true`。如果上游未开通工具,Responses 可能先吐自然语言再返回 `ToolNotOpen`,这类报错应按工具不可用排查,不要先当成 JSON 解析 bug。 + ### 手机验证码短信 手机验证码发送走阿里云普通短信 `SendSms`,验证码由 `module-auth` 在当前 `api-server` 进程内生成、哈希存储和校验,不再调用阿里云托管验证码的 `SendSmsVerifyCode` / `CheckSmsVerifyCode`。因此 `api-server` 重启后,已发送但未校验的验证码会失效。 @@ -246,6 +280,16 @@ cargo test -p platform-auth --manifest-path server-rs/Cargo.toml aliyun_send_sms 个人任务首版 scope 仅支持 `user`。后台、RPG、大鱼吃小鱼、Visual Novel、Story、Combat 等特定链路按 tracking 中间件排除规则处理;作品游玩统一使用 `work_play_start`。 +外部 API 失败审计复用 `tracking_event`,不新增表。失败事件优先写入本机 tracking outbox,再由后台 worker 批量落库;如果 outbox 因权限、磁盘或保护阈值不可写,会回退同步直写 SpacetimeDB。`metadata_json` 包含 endpoint、operation、failureStage、statusCode、statusClass、timeout、retryable、errorMessage、latencyMs、promptChars、referenceImageCount、imageModel 和 rawExcerpt。常用查询: + +```sql +SELECT event_id, scope_id AS provider, metadata_json, occurred_at +FROM tracking_event +WHERE event_key = 'external_api_call_failure' +ORDER BY occurred_at DESC +LIMIT 50; +``` + tracking outbox 默认配置: ```env @@ -258,6 +302,17 @@ GENARRATIVE_TRACKING_OUTBOX_MAX_BYTES=268435456 outbox 采用 NDJSON 文件保存原始事件。达到 `BATCH_SIZE` 时会立刻把当前 active 文件原子封存为 sealed 文件,并马上切到新的 active 继续写入;后台 worker 异步 flush sealed 文件,HTTP 请求线程不等待 SpacetimeDB。`FLUSH_INTERVAL_MS` 只负责兜底封存长时间未满批的 active 文件。SpacetimeDB 批量 procedure 返回成功后删除 sealed 文件,失败则保留文件并重试。`MAX_BYTES` 是磁盘保护阈值,不是 flush 阈值;超过后低价值 route tracking 可以被丢弃并记录日志 / 指标,关键同步事件不进入该丢弃路径。sealed 文件若出现无法解析的坏行,会重命名为 `corrupt-*` 隔离并记录 `genarrative.tracking_outbox.files.corrupt` 指标,避免一个坏文件阻塞后续批量入库。该机制提供至少一次投递语义,依赖 `tracking_event.event_id` 幂等跳过重复事件。 +release 机器如果日志每秒刷 `tracking outbox ... Permission denied (os error 13)`,先检查 `/etc/genarrative/api-server.env` 是否缺少 `GENARRATIVE_TRACKING_OUTBOX_DIR`。缺少时 `api-server` 会回退到本地开发默认相对路径 `server-rs/.data/tracking-outbox`,而 systemd 的工作目录是只读发布目录 `/opt/genarrative/releases/`,`genarrative` 用户无法在其中创建 `server-rs`。修复顺序: + +```bash +install -d -o genarrative -g genarrative -m 0750 /var/lib/genarrative/tracking-outbox +grep -n '^GENARRATIVE_TRACKING_OUTBOX' /etc/genarrative/api-server.env +systemctl restart genarrative-api.service +journalctl -u genarrative-api.service --since '30 seconds ago' --no-pager | grep -E 'tracking outbox|Permission denied|os error 13' +``` + +`Genarrative-Server-Provision` 和 `Genarrative-Api-Deploy` 会在保留旧 `/etc/genarrative/api-server.env` 的前提下补齐缺失的 tracking outbox 与 auth-store 运行态路径,并确保 `/var/lib/genarrative/tracking-outbox`、`/var/lib/genarrative/auth` 归属 `genarrative:genarrative`。 + 常用检查思路: ```sql diff --git a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md index d7b97986..4d2be6e8 100644 --- a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md +++ b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md @@ -10,6 +10,22 @@ `PlatformEntryFlowShellImpl.tsx` 仍是平台入口编排壳,后续维护时应优先把独立 UI 片段、公开作品映射、草稿生成 notice 和运行态状态 helper 拆到 `src/components/platform-entry/PlatformEntryFlowShellImpl/` 或同目录紧邻 helper 文件。拆分只允许改变文件组织,不改变入口配置事实源、默认导出、props、页面阶段、UI 文案或现有交互;其中拼图首访 onboarding 已拆为 `PlatformEntryFlowShellImpl/PuzzleOnboardingView.tsx`。 +移动端底部一级导航是全局平台样式,不按单一玩法分叉。当前视觉统一为米白浮动胶囊底座、浅棕分隔线、棕色线性图标、橘色选中态和底部短下划线;中间 `创作` 入口保持凸起圆形主按钮,但凸起位移只能作用在按钮内容层,不能移动承载分隔线的 Tab 按钮容器,确保创作左右分隔线与其他分隔线垂直位置一致。Tab 名称和可见性仍由现有 `PlatformHomeTab` / 登录态规则决定,样式调整不得改写 Tab 文案或导航状态。 + +## 新增玩法创作工具平台 SOP + +新增玩法默认采用表单/图片输入创作工作台,链路为: + +```text +创作入口 -> 工作台 -> 生成页 -> 结果页 -> 试玩 -> 发布 -> 运行态 +``` + +默认工作台只提交结构化表单、图片槽位和配置 payload,不默认增加聊天输入区、流式消息区或轻输入 Agent。确需偏离该模式时,必须先在 PRD 和本文档写明例外原因、影响范围和回退方式,再进入编码。 + +单图资产编辑统一通过 `CreativeImageInputPanel` 承载上传、AI 重绘、参考图、历史图和删除确认;新玩法页面不得重复手写这些交互。系列素材图集生成统一走“批量规划 -> sheet 生图 -> 后端切图 -> 透明化 -> OSS 持久化 -> 状态回写 -> 局部重生成”流程,玩法只提供 `sheetSpec`、`slotSpecs`、提示词和字段映射,不把任一玩法专属素材 DTO 当作平台通用模型。 + +`api-server` 的 `generated_asset_sheets` 是当前通用系列素材图集模块:`n` 是必选参数,模块负责组装 `n*n` sheet prompt、按 `n*n` 切片、绿幕 / 近白底透明化、导出 PNG 和 OSS 持久化请求。物品名称 prompt 和特殊设定 prompt 是可选输入;调用方可传入类似“每个物品生成五个不同视图”的视角约束,通用模块会把 sheet prompt、物品行 prompt、特殊设定 prompt 编码写入 OSS 元数据。玩法仍负责计费、物品规划、slot 映射、失败回写和把通用切片结果映射回自己的草稿 / profile / runtime 字段。 + ## 草稿与作品架 1. 草稿页作品卡对齐发现页列表卡风格:左侧信息,右侧封面图,移动端单列,桌面两到三列。 @@ -20,6 +36,34 @@ 6. 点击 `generationStatus=generating` 的草稿卡必须恢复对应玩法的生成进度页,不能进入空白结果页或普通工作区;恢复生成页的 `startedAtMs` 使用作品摘要 `updatedAt` 推导。 7. 私有 generated 图片必须通过 `ResolvedAssetImage` / `/api/assets/read-url` 换签读取。 +## RPG / 自定义世界 + +当前 RPG 创作入口使用 `playId = rpg`,工程域和运行态源类型沿用历史 `custom-world`。默认入口状态为 `visible=true`、`open=true`,对外展示为“文字冒险”;`airp` 仍是独立的“AI RPG”占位入口,保持 `open=false`,不要把它当作当前 RPG 创作链路开放。 + +当前链路为: + +```text +创作入口 -> RPG Agent 共创工作台 -> 生成过程页 -> 结果页 -> 进入世界/试玩 -> 发布 -> RPG 运行态 +``` + +RPG 是历史既有链路例外:当前仍使用对话式 Agent 共创工作台和 RPG 资产编辑器体系,不作为新增玩法默认模板复制。新增玩法继续遵循本文默认的表单/图片输入工作台、`CreativeImageInputPanel` 单图槽位和通用系列素材图集生成流程;如果要把 RPG 逐步迁回默认模式,应先补 PRD 和迁移方案,再改代码。 + +RPG API 仍沿用历史命名空间:`/api/runtime/custom-world*`、`/api/story/*`、`/api/runtime/chat/*`。这些路由在 `api-server` 入口熔断中统一映射到 `rpg`,只按 `open` 判断是否允许调用;`visible` 只控制创作页入口展示和作品架可见性。 + +RPG Agent 结果页点击发布或发布并进入世界时,必须先把结果页当前 profile 通过 `sync_result_profile` 保存回 `custom_world_agent_session.draft_profile_json`,再发送发布动作;发布动作前端契约只允许提交 `{ action: 'publish_world' }`,`api-server` 只补作者公开信息,不转发 `profile`、`draftProfile`、`legacyResultProfile` 或 `settingText`。`spacetime-module` 发布时只读取当前 session 的 `draft_profile_json` 作为草稿真相,从 `settingText`、`creatorIntent.rawSettingText`、`creatorIntent.worldHook`、`worldHook`、`anchorContent.worldPromise(.hook)`、`summary`、`name/title` 依次派生正式 `setting_text`,最后才回退 `seed_text`。不要把 `seed_text` 当作唯一设定来源,旧会话可能为空。 + +Agent session 已进入 `published` 后,结果页按钮只能执行“进入世界”:前端需先通过 `result-view` 回读已发布 profile 并启动运行态,不得再次调用 `sync_result_profile` 或发送 `{ action: 'publish_world' }`。`publish_world` 只允许在 `object_refining`、`visual_refining`、`long_tail_review`、`ready_to_publish` 等发布前阶段触发;否则会被后端阶段门槛拒绝。 + +`legacyResultProfile` 只作为历史结果页 profile 兼容兜底;编译正式 profile 时,session 草稿内已保存字段优先于 legacy 字段,legacy 只能补缺失字段。`publish_world` 不再接受前端临时传入的 legacy 载荷;历史兼容路径中 legacy 缺省或显式为 `null` 时等价于未提供,不得因此报 `custom_world.compile.legacy_result_profile_json 不是合法 JSON object`。真正的数组、字符串、数字等非 object legacy 载荷仍应拒绝。 + +RPG 结果页开局 CG 是 `profile.openingCg` 资产槽位:`api-server` 负责 VectorEngine / OSS 副作用并返回故事板和视频引用,前端只把结果写回当前 profile;`sync_result_profile`、作品库保存和 `normalizeCustomWorldProfileRecord` 都必须保留该槽位。封面是 `profile.cover` 资产槽位,默认封面也要保留 `sourceType='default'` 和 `characterRoleIds`,不能因为没有 `imageSrc` 就当作空封面。若生成成功后画面短暂显示又变回空白,优先检查父层重新同步或 profile 归一化是否把 `openingCg` / `cover` 丢掉,而不是先怀疑已生成资源本身失效。 + +RPG 从作品架、广场详情或作品号搜索点击“启动”前,入口 client 必须把后端返回的完整 `profile` 先经过 `normalizeCustomWorldProfileRecord`,并用作品条目的 `profileId/worldName/subtitle/summaryText` 补齐旧数据缺失字段;运行态和详情页不得直接消费未归一化的旧 profile。作品架列表或 `savedCustomWorldEntries` 中的摘要 profile 只可用于卡片展示,不可在详情接口已回读完整 profile 后覆盖 `selectedDetailEntry`;若摘要缺少 `playableNpcs`、`storyNpcs`、`landmarks`、`items`、`sceneChapterBlueprints`、`cover`、`openingCg`、`skills[].actionPreviewConfig`、`initialItems[].iconSrc`、`attributeSchema`、角色 `attributeProfile`、场景残留或场景幕背景资产,启动和编辑必须继续使用详情 profile,否则会进入默认角色 / 默认 profile,或在编辑页丢 CG、封面、技能预览和初始物品图标。正式“进入世界”发布 / 回读结果页时,同一 `profile.id` 下也不得用字段更少的后端旧视图降级当前结果页完整 profile。角色选择页还需要在角色数组异常或为空时回退默认角色,并显示可返回的轻量空态,不能 `return null` 造成黑屏。运行态懒加载 fallback 必须可见,不能用纯 `null` 让用户误判为黑屏。 + +RPG 运行态的战斗终局、继续冒险、继续探索和切场景都属于服务端 runtime 快照真相:`module-runtime-story` 必须在终局战斗 action 后调用 post-battle finalization,持久写入 `story_continue_adventure`、`deferredOptions`、`deferredRuntimeState.storyEngineMemory.currentSceneActState` 和清理后的战斗状态;`idle_travel_next_scene` / `camp_travel_home_scene` 必须由后端写入新的 `currentScenePreset`、`currentSceneActState`、`currentEncounter` 和 `runtimeStats.scenesTraveled`。前端只播放退场、进场和继续按钮表现,不能用默认 `观察/试探/调息` fallback 或本地动画假装推进剧情。旧 bootstrap 快照可能只有 `connectedSceneIds` / `forwardSceneId` 而没有 `connections`,后端生成战后旅行选项时必须兼容这些字段。 + +RPG / 拼图等运行态存档选择入口统一在个人中心 `常用功能 > 存档` 暴露为独立弹窗;“玩过”弹窗可以继续合并展示可继续存档,但不能成为唯一入口。前端只展示 `/api/profile/save-archives` 返回的列表并在用户选择后调用对应恢复接口,不能本地拼装或筛选正式存档真相。 + ## 拼图 当前拼图链路: @@ -31,25 +75,51 @@ 当前口径: - 图像输入复用 `CreativeImageInputPanel`。 -- 支持画面描述生图、多参考图生图、上传或历史生成主图后 AI 重绘、上传或历史生成主图后不重绘;关闭 AI 重绘时,前端可提交本地上传 Data URL 或历史 `/generated-*` 图片路径,后端统一解析为首关正式图后再持久化。 -- 草稿生成会先持久化 `generationStatus=generating` 的作品摘要,生成完成并回写关卡图、UI 背景后再变为 `ready`;首关关卡图和 UI 背景在命名稳定后并行启动,当前不自动生成背景音乐。 +- 结果页每关画面编辑复用 `CreativeImageInputPanel`;入口页和关卡画面只共享受控 UI 模块,不共享数据源、状态、action 或存储位置:入口页继续写 `formDraft` 与草稿编译 payload,关卡画面写 `levels[].pictureReference/pictureDescription` 并触发 `generate_puzzle_images`。结果页删除独立“素材配置”Tab,不再提供单独 UI 背景生成入口。通用图片面板的展示图和 AI 重绘参考图能力必须分开控制:结果页正式关卡图只作为预览图,不因存在正式图自动暴露 AI 重绘开关;只有本地上传、历史选择或已保存 `pictureReference` 可作为重绘参考图时,才显示 AI 重绘开关并把状态带入 `generate_puzzle_images`。用户在本次编辑中上传或选择历史图后,该图优先占据主图卡片,可删除、切换 AI 重绘,也可关闭 AI 重绘直用;仅有正式图预览时,画面描述框仍可上传多张参考图。关卡详情弹窗应使用加宽面板,关卡名称、画面图和画面描述合并在同一个纵向列表中,名称输入和画面编辑模块外层不再包独立 `platform-subpanel`;画面图卡仍必须保留稳定最小高度,避免弹窗内 `flex-1` 布局坍缩后只剩标题、描述输入和操作按钮。 +- 支持画面描述生图、多参考图生图、上传或历史生成主图后 AI 重绘、上传或历史生成主图后不重绘;主链要求浏览器先经 `/api/assets/direct-upload-tickets` 直传 OSS 并确认 `asset_object`,创作 action 只提交 `referenceImageAssetObjectId(s)`,由后端校验 owner / bucket / kind / MIME / size 后签发 OSS 只读 URL 并下载为 VectorEngine `/v1/images/edits` 的 multipart `image` part。本地上传 Data URL 与历史 `/generated-*` 图片路径仅保留为旧草稿、旧入口或未迁移客户端的兼容输入;关闭 AI 重绘时,后端统一解析为首关或当前关卡正式图后再持久化,不调用第一段拼图首图生成。 +- 草稿生成会先持久化 `generationStatus=generating` 的作品摘要,生成完成并回写关卡拼图画面、关卡画面参考图、UI spritesheet 和关卡背景图后再变为 `ready`;当前不自动生成背景音乐。生成页进度不再按固定 5 分钟展示,而按实际开始时间和当前路径的分步骤预计时长推进;任一同步 action 回包到达时立即以真实完成/失败结果冻结进度。 - 作品架拼图草稿的“生成中”遮罩只表示初始草稿还没有可查看结果;只要作品摘要、首关封面或任一关卡候选图已经可用,后续 UI 背景重生成和追加关卡生图都必须作为结果页局部生成态处理,不能阻止打开草稿结果页。 -- 拼图草稿编译是长耗时 action,前端 action 请求默认等待 `1_000_000ms` 且不自动重试,生成页预计完成时间按 `5` 分钟展示;生成页恢复时必须沿用作品摘要 `updatedAt` 作为原始 `startedAtMs`,失败/完成态用 `finishedAtMs` 冻结耗时,不能在锁屏或返回草稿页后重新从 0 计时。 +- 拼图草稿编译是长耗时 action,前端 action 请求默认等待 `1_800_000ms`(30 分钟)且不自动重试。每次 `gpt-image-2` 调用的预期用时按 90 秒计算;完整 AI 重绘路径为 `编译首关草稿` 8 秒、`生成关卡名称` 10 秒、`生成拼图首图` 90 秒、`生成关卡画面` 90 秒、`生成UI与背景` 90 秒、`写入正式草稿` 10 秒,合计约 298 秒。上传图且关闭 AI 重绘时必须跳过 `生成拼图首图`,直接进入 `生成关卡画面` 和 `生成UI与背景`,合计约 208 秒。生成页恢复时必须沿用作品摘要 `updatedAt` 作为原始 `startedAtMs`,失败/完成态用 `finishedAtMs` 冻结耗时,不能在锁屏或返回草稿页后重新从 0 计时。 - 若浏览器锁屏、息屏或网络切换导致 compile 请求失败,前端在标记失败前必须先复读 `getPuzzleAgentSession(sessionId)`;只有最新 session 仍缺 `draft.coverImageSrc`、首关 `coverImageSrc` 或候选图时才展示失败,复读到已生成草稿时按成功收尾、刷新作品架并继续自动试玩/结果页链路。 -- 拼图参考图 AI 重绘优先走 VectorEngine `/v1/images/edits`;若编辑接口超时,`api-server` 会降级为 `/v1/images/generations`,并把同一参考图塞进 `image` 数组继续生成,避免参考图草稿整单失败。 -- 结果页素材配置当前只保留 UI 相关能力;旧背景音乐入口隐藏。 +- 拼图参考图 AI 重绘走 VectorEngine `/v1/images/edits`;无参考图时走 `/v1/images/generations`。两者模型都使用 `gpt-image-2`,参考图由后端作为 multipart `image` part 传入编辑接口。 +- 每次新建关卡生成或重新生成关卡图都必须由 `api-server` 串起当前关卡资产包:AI 重绘开启时第一段沿用草稿生成第一关的拼图主图提示词配置和模型 / 尺寸 / 参考图规则生成 `coverImageSrc/coverAssetId` 作为关卡拼图画面和结果页预览图,提示词来源同样按显式画面描述、关卡画面描述、草稿摘要顺序回退,且固定要求输出画面比例为 `1:1`;上传图且关闭 AI 重绘时跳过这一段,把上传图或历史图持久化为 `sourceType=uploaded` 的正式候选。随后用正式候选图作为参考,`9:16` 生成完整拼图游戏关卡画面并写入 `levelSceneImageSrc/levelSceneImageObjectKey`,提示词必须要求道具按钮上不要显示次数标注,且返回按钮和设置按钮旁禁止标注文字;UI spritesheet 与关卡纯背景在关卡画面完成后并发生成,spritesheet 用 `1:1`、`1k` 先生成纯绿色绿幕背景图,后端上传 OSS 前必须把绿幕扣成透明 PNG,再写入 `uiSpritesheetImageSrc/uiSpritesheetImageObjectKey`,按钮顺序固定为返回、设置、下一关、提示、原图、冻结,按钮素材自身保留对应中文文字,返回和设置按钮不得额外生成白色外圈、白底圆环或浮雕外框;纯背景用 `9:16`、`1k` 写入 `levelBackgroundImageSrc/levelBackgroundImageObjectKey`,提示词必须包含“禁止在背景中出现人像或和拼图画面中主体一致的内容”。运行态不直接使用第二段完整关卡画面,但必须持久化它用于追踪和后续再生成。结果页局部关卡生成进度按 AI 重绘开启约 270 秒、关闭 AI 重绘约 180 秒展示。 - 结果页允许多关卡并行编辑和生成;某一关卡图片生成完成回包只静默更新该关卡素材与生成态,不得自动打开或切换关卡详情面板,避免打断用户正在编辑的其它关卡。 -- 结果页 UI 背景重生成只禁用 UI 背景自己的按钮和确认动作,不禁用“新增关卡”、关卡图片生成、关卡详情编辑和结果页导航;关卡图片生成也只标记对应关卡的局部生成进度。 +- 结果页关卡图片生成只标记对应关卡的局部生成进度,不禁用“新增关卡”、其它关卡详情编辑和结果页导航。 +- 结果页单关测试只能把完整草稿持久化,并通过 `levelId` 指定运行态起始关卡;不得把单关快照作为整份草稿调用 `updatePuzzleWork`,否则 source session 和作品 profile 的 `levels` 会被覆盖成单关,退出重进后其它关卡会丢失。 - 结果页生成关卡图时若关卡名为空,前端必须传 `shouldAutoNameLevel=true`,后端复用首关命名契约先按画面描述生成关卡名,再在图片生成后用视觉命名结果精修,并把生成名和 UI 背景提示词随本次关卡快照写回。 -- 拼图 UI 背景是作品运行态背景,不只属于第一关;本地试玩、直达指定关卡和正式 `next-level` 推进时,目标关卡缺 `uiBackgroundImageSrc/uiBackgroundImageObjectKey` 必须继承同作品首个可用 UI 背景,仍缺失时才沿用当前运行态快照背景或默认 UI。 +- 拼图运行态背景优先读取当前关卡 `levelBackgroundImageSrc/levelBackgroundImageObjectKey`,旧数据才兼容 `uiBackgroundImageSrc/uiBackgroundImageObjectKey`;本地试玩、直达指定关卡和正式 `next-level` 推进时,目标关卡缺关卡背景时必须继承同作品首个可用关卡背景,仍缺失时才沿用当前运行态快照背景或默认 UI。运行态按钮视觉优先读取当前关卡 `uiSpritesheetImageSrc/uiSpritesheetImageObjectKey`,先按透明 alpha 自动边界检测识别 spritesheet 中的独立按钮展示矩形,再按原图位置从左到右、从上到下映射到返回、设置、下一关、提示、原图、冻结;同一组件还要按较高 alpha 阈值派生紧致点击热区,透明留白和柔边低 alpha 区域尽量不响应点击。检测失败时回退旧固定六格裁切,缺失时才用现有图标按钮兜底。有 spritesheet 时,返回和设置按钮的点击容器只提供透明点击区,不再叠加默认白色圆形底;底部提示、原图、冻结三枚素材按检测矩形的原始宽高比显示,不能强行拉伸成正圆或铺满整列。底部道具区不再使用连片胶囊背景,提示、原图、冻结三个按钮均匀分布;运行态只展示按钮素材本身,不额外叠加“提示 / 原图 / 冻结”文字。 - 拼图运行态棋盘不叠加分块蒙版、描边、阴影、选中底色或合并块 SVG 轮廓;拼图片本体需要裁切为圆角形状,单块使用独立圆角裁切,合并块使用 SVG 原生 `clipPath` 裁切整体外轮廓,外凸角和内凹角分别计算半径,内凹角半径要比外凸角更明显以避免手机 WebView 中看起来仍是直角。原图道具只在用户主动确认后打开独立原图查看层,不在当前拼图棋盘上叠加原图。 - 拼图运行态拖拽必须完全跟随手指或鼠标位置,`pointermove` 期间即时写入可见拼块的 transform,不依赖等待后端回包、React 重渲染或下一帧动画队列;进入拖动后不展示拼块选中态或“已选择”提示,松手后再提交目标格同步规则真相。 - 拼图运行态的提示、设置等点击弹层跟随当前运行态主色主题,使用普通圆角主题面板,不复用像素九宫格素材框。 - 拼图运行态壳层自身要补齐 `platform-ui-shell` / `platform-theme` / `platform-theme--light|dark`,不能依赖外层平台壳来提供主题变量;`/puzzle` 直达页和平台内嵌页都必须渲染同一套主题语义类。 +- 拼图运行态顶部关卡信息采用游戏化铭牌样式:橘棕横向关卡名牌承载 `第 N 关` 和关卡名,左侧固定使用 `media/logo.png` 卡通形象;倒计时作为下挂米白小牌独立显示,紧贴铭牌但不遮挡棋盘。该样式只改变运行态 HUD 视觉,不改变计时、暂停、失败同步或关卡推进规则。 - 拼图运行态进行中关卡的 `elapsedMs` 仍是结算字段,设置面板的“当前用时”必须按 `startedAtMs`、暂停累计和冻结累计实时派生;不要直接把进行中的 `currentLevel.elapsedMs` 当作展示值。 +- 推荐页嵌入拼图运行态时,通关结算弹层必须挂到页面级 fixed 浮层,不能留在推荐卡片视觉区内的 absolute 覆盖层;推荐页滑动卡片和运行态视口都使用 `overflow: hidden`,半屏内容区会裁剪排行榜、下一关按钮和相似作品卡。 - 推荐页里的拼图作品如果从运行态进入“改造”结果页,返回平台后要清掉推荐嵌入态的 `activeRecommendEntryKey` / `activeRecommendRuntimeKind` / `isStartingRecommendEntry`,再重新按推荐页自动启动逻辑进入作品,不能复用已经被清空的旧 `puzzleRun`。 - 拼图运行态允许前端低延迟交互表现,但通关、排行榜、奖励和作品状态仍以后端确认为准。 +## 跳一跳 + +对外名称:`跳一跳`。工程域:`jump-hop`。PRD 见 `docs/prd/【玩法创作】跳一跳俯视角玩法模板PRD-2026-05-19.md`。 + +首版定位为俯视角 / 等距视角 2D 休闲跳跃模板,链路对齐拼图的创作闭环: + +```text +创作入口 -> 模板输入 -> 生成过程页 -> 结果页 -> 试玩 -> 发布 -> 运行态 +``` + +素材生成规则固定为: + +1. 初始草稿生成时,角色形象单独调用一次生图; +2. 初始草稿生成时,地块单独调用一次生图,输出 3D 视图的 2D 图片图集; +3. 地块图集由后端切分为起点、普通、目标、终点等透明 PNG; +4. 封面和分享图由角色图与地块图轻量合成,不再额外调用第三次生图; +5. 显式重生成角色或地块时,只重生成对应资产槽位。 + +运行态规则真相必须沉到 `module-jump-hop`,前端只做蓄力表现、角色位移、投影和落地反馈。通关、失败、分数、combo、运行态快照和发布作品状态以后端为准。公开列表应走 `jump_hop_gallery_card_view` 订阅缓存,不要每次 HTTP 请求调用 procedure 组装全量列表。 + +平台首页推荐、精选、最新、公开详情、搜索、已玩作品和公开试玩统一按 `sourceType='jump-hop'` 与 `JH-*` 公开作品号识别跳一跳作品;从公开详情或推荐流启动运行态时,若卡片摘要不足以携带角色图、地块图集和路径配置,必须先补读完整 work profile 再传入运行态。 + ## 抓大鹅 Match3D 对外名称:`抓大鹅`。工程域:`match3d`。 @@ -57,9 +127,10 @@ 入口表单只展示: - 题材主题。 -- `2D素材风格` 横向风格卡:扁平图标、赛璐璐卡通、像素复古、手绘水彩、贴纸描边、厚涂图标、自定义。 - 难度:轻松、标准、进阶、硬核。 +入口不再要求用户选择素材风格;历史草稿和旧接口中的 `assetStyleId` / `assetStyleLabel` / `assetStylePrompt` 仅作为兼容字段保留,新入口提交不再写入这些字段。 + 难度映射: | 难度 | clearCount | difficulty | 总物品数 | 物品种类 | @@ -67,28 +138,27 @@ | 轻松 | 8 | 2 | 24 | 3 | | 标准 | 12 | 4 | 36 | 9 | | 进阶 | 16 | 6 | 48 | 15 | -| 硬核 | 21 | 8 | 63 | 21 | +| 硬核 | 21 | 8 | 63 | 20 | 当前素材生成流水线: 1. 点击生成前弹出泥点确认,草稿生成固定消耗 `10` 泥点。 -2. 先写入可恢复草稿 profile,再执行文本计划、图片生成、切图、OSS 上传、背景和容器生成;作品摘要在素材或背景未完整时下发 `generationStatus=generating`,素材和背景完整后下发 `ready`,草稿完成条件不包含 `backgroundMusic`。 -3. 物品素材不再调用 Hyper3D Rodin,不再生成 GLB。新草稿和批量新增固定生成 2D 五视角素材。 -4. 物品 sheet 走 VectorEngine Gemini `gemini-3-pro-image-preview` 原生 `generateContent`,单张 `1:1` 图固定 `5*5`,每张承载 `5` 个物品、每个物品 `5` 个视角。 -5. 切图前先在整张 sheet 上做绿幕 / 近白底透明化和边缘去污染,再按格子导出独立 PNG;每个视角图再以扩大的 PNG 边界带为种子,把连通的浅绿 / 近白抗锯齿边直接改为透明,并对贴透明背景的弱绿 / 暗绿轮廓像素做去绿污染处理,最后按剩余可见主体二次收紧;不要先裁剪单格再各自去绿。 -6. `generatedItemAssets[].imageViews[]` 是新素材主字段,`imageSrc/imageObjectKey` 只兼容首张视角。 -7. 文本生成物品名称时必须同时生成 `itemSize`,只允许 `大`、`中`、`小`。该字段随 `generatedItemAssets[].itemSize` 持久化并下发;历史缺失字段的素材按 `大` 兼容,模型缺失或非法值按物品名本地推断。 -8. 局内 `9:16` 纯背景图和 `1:1` 中心容器 UI 图分开生成。纯背景不得包含锅、盘、托盘、HUD、按钮、文字或物品,且入库前必须合成为全画幅不透明图片,不允许出现透明区域;容器图走 `/v1/images/edits` 参考透明容器图。 +2. 先写入可恢复草稿 profile,再执行文本计划、关卡整图生成、三张派生图生成、OSS 上传和素材解析;作品摘要在背景、UI spritesheet 或物品 spritesheet 未完整时下发 `generationStatus=generating`,完整后下发 `ready`,草稿完成条件不包含 `backgroundMusic`。 +3. 首次调用 VectorEngine `gpt-image-2`,无参考图,竖屏 `9:16`,生成完整抓大鹅关卡画面并持久化到 `generatedBackgroundAsset.levelSceneImageSrc/levelSceneImageObjectKey`。提示词必须包含用户主题描述、顶部返回 / 标题倒计时 / 设置按钮、中间与主题匹配且贴横向边缘的容器,以及底部“移出 / 凑齐 / 打乱”三个道具按钮。 +4. 关卡整图完成后并发发起三次 `gpt-image-2` 编辑请求,三者都以关卡整图作为参考图:`1K`、`1:1` 的 UI spritesheet 写入 `uiSpritesheetImageSrc/uiSpritesheetImageObjectKey`;`1K`、`9:16` 的背景图写入 `imageSrc/imageObjectKey`;`2K`、`1:1` 的物品 spritesheet 写入 `itemSpritesheetImageSrc/itemSpritesheetImageObjectKey`。 +5. UI spritesheet 提示词固定要求按从上到下、从左到右整理纯绿色绿幕背景素材:返回按钮、设置按钮、方格素材(不含边框,仅保留一个)、移出按钮、凑齐按钮、打乱按钮;后端上传 OSS 前必须把绿幕扣成透明 PNG。背景图提示词固定要求移除全部 UI 组件和容器内含物,完整保留容器和背景,并补全被 UI 覆盖的背景内容。 +6. 物品 spritesheet 固定 `10行*10列`、统一纯绿色绿幕背景,后端上传 OSS 前必须把绿幕扣成透明 PNG;素材间距严格均匀分布,每一行包含两种物品,每种物品五个不同形态,物品来自参考图中心容器中的 2D 素材,严禁高相似度物品。新流程每次固定解析并持久化 `20` 种物品,物品信息列表全部展示这 `20` 种;持久化单格映射必须按 `row = itemIndex / 2 + 1`、`col = itemIndex % 2 * 5 + viewIndex + 1` 写入通用系列素材图集,不能再用 `row = itemIndex + 1`。`generatedItemAssets[].imageViews[]` 仍兼容已切好的五视角图,缺失时运行态和编辑器按 spritesheet 自动解析结果回退。 +7. 前端和运行态统一使用 alpha 连通域矩形检测解析 spritesheet:UI 图按返回、设置、方格、移出、凑齐、打乱顺序映射回原 UI 位置;物品图按检测顺序每 `5` 个区域组成一个物品的五个形态,最多 `20` 个物品。透明背景是解析前提,不能在前端按固定像素坐标写死切片。 +8. 文本生成物品名称时必须同时生成 `itemSize`,只允许 `大`、`中`、`小`。该字段随 `generatedItemAssets[].itemSize` 持久化并下发;历史缺失字段的素材按 `大` 兼容,模型缺失或非法值按物品名本地推断。 9. 当前抓大鹅音频生成关闭:入口无 `生成音效`,草稿不生成背景音乐或点击音效,结果页不展示背景音乐 Tab 或点击音效生成入口。历史 `backgroundMusic` / `clickSound` 字段继续兼容传递。 -10. UI 背景和容器资产的持久化真相仍在 `generatedItemAssets[].backgroundAsset`;Agent session、work summary/detail、结果页和运行态入口都必须把该字段提升为 `backgroundImageSrc/backgroundImageObjectKey/generatedBackgroundAsset` 读取。草稿编译后的 `draftJson` 自身也必须携带 `generatedItemAssets` 快照;HTTP facade 不能只依赖 work detail 回读补齐 UI 资产,外部回读为空时也不得清空草稿内已有的背景 / 容器图。平台壳层从作品架、广场、生成完成回调、结果页保存 / 发布 / 试玩回调进入 Match3D profile 时也要先归一化并提升,避免首次试玩、手动试玩、推荐流或公开详情运行态退回默认背景 / 默认容器。 +10. 背景、UI spritesheet、物品 spritesheet 和历史容器兼容字段的持久化真相仍在 `generatedItemAssets[].backgroundAsset` 与提升后的 `generatedBackgroundAsset`;Agent session、work summary/detail、结果页和运行态入口都必须把该字段提升为 `backgroundImageSrc/backgroundImageObjectKey/generatedBackgroundAsset` 读取。草稿编译后的 `draftJson` 自身也必须携带 `generatedItemAssets` 快照;HTTP facade 不能只依赖 work detail 回读补齐 UI 资产,外部回读为空时也不得清空草稿内已有的背景 / 图集。平台壳层从作品架、广场、生成完成回调、结果页保存 / 发布 / 试玩回调进入 Match3D profile 时也要先归一化并提升,避免首次试玩、手动试玩、推荐流或公开详情运行态退回默认背景。 结果页当前结构: - `作品信息`:名称、描述、标签;封面编辑收口到发布面板。 - `难度配置`:四档离散拖动条,显示需要消除、总物品数、物品种类、已生成物品种类。 -- `素材配置 > 物品`:两列素材卡,点击打开独立五视角预览面板;支持删除、批量新增和批量重新生成。替换模式必须保留原 `itemId` 和列表顺序。 -- `素材配置 > UI`:纯背景图与运行态 UI 预览,重生成消耗 `2` 泥点;UI 预览必须复用运行态顶部 HUD、中央容器棋盘、容器图定位和底部槽位样式,不单独维护一套简化预览 UI。 -- `素材配置 > 容器形象`:单独预览和重生成中心容器,消耗 `2` 泥点。 +- `素材配置 > 物品`:两列素材卡固定展示 20 个物品,点击打开独立五视角预览面板;支持删除、批量新增和批量重新生成。替换模式必须保留原 `itemId` 和列表顺序。 +- `素材配置 > UI素材`:预览背景图、UI spritesheet 原图、物品 spritesheet 原图和物品 spritesheet 自动解析缩略图;背景图只支持预览,不提供重新生成入口。UI 预览必须复用运行态顶部 HUD、中央容器棋盘和底部槽位样式,不单独维护一套简化预览 UI。 运行态当前口径: @@ -98,8 +168,9 @@ - 初始物品坐标围绕容器口中心生成,并保留内缩安全距离,避免贴边和局部角落聚集。 - 本地试玩与 Rust `module-match3d` 后端领域生成使用同一套中心铺开口径;生成点覆盖四象限且均值接近中心。 - 运行态优先消费 2D 生成图;默认积木 / 程序化 3D 表现只作为视觉分支和兜底,不改变规则真相。 -- 运行态启动前要预加载 `generatedItemAssets[].imageViews[]`、顶层 `generatedBackgroundAsset`、物品挂载 `backgroundAsset` 中的背景和容器图;首次生成自动试玩、结果页手动试玩、推荐流和公开详情启动都必须传入提升后的 profile。卡片摘要缺 UI 背景或容器字段时,进入运行态前必须补读 work detail。补读后的 profile 也要再次提升 `generatedItemAssets[].backgroundAsset`,确保背景和容器字段传给 `Match3DRuntimeShell`。 -- 局内容器图在移动端宽度大于屏幕宽度并略向下压,当前运行态使用 `w-[min(116vw,42rem)]` 与 `top-[54%]` 放大和下移容器图本体,保持原图比例不拉伸且不改变后端物品布局、点击半径或消除规则;生成容器图加载成功后棋盘外壳透明且 `overflow-visible`,只有生成图缺失或加载失败时才显示透明参考容器兜底。 +- 难度只决定本局加载的物品种类数量:轻松 3、标准 9、进阶 15、硬核 20。硬核仍保留 21 次消除和 63 件总物品,运行态按 20 种素材循环复用,不要求生成第 21 种素材。 +- 运行态启动前要预加载 `generatedItemAssets[].imageViews[]`、顶层 `generatedBackgroundAsset`、物品挂载 `backgroundAsset` 中的背景、UI spritesheet 和物品 spritesheet;首次生成自动试玩、结果页手动试玩、推荐流和公开详情启动都必须传入提升后的 profile。卡片摘要缺图集字段时,进入运行态前必须补读 work detail。补读后的 profile 也要再次提升 `generatedItemAssets[].backgroundAsset`,确保背景和图集字段传给 `Match3DRuntimeShell`。 +- 背景图作为运行态全屏背景,图内已经保留容器;旧 `containerImage*` 只作为历史透明容器兼容字段。若 `containerImage*` 与 `uiSpritesheetImage*` 同源,运行态不得把 UI spritesheet 当中心容器图叠到棋盘上。 - generated 私有图换签未完成时,局内物品先隐藏等待,不得短暂显示默认积木;同一批资源在重启 run 时保留已解析签名 URL,只有资源源列表变化或换签失败后才允许进入兜底视觉。 - `itemSize` 只缩放生成 2D 图片本体:`大`、`中`、`小` 均按相对尺寸缩放,其中 `大` 也比原始图片略小,`中` 和 `小` 进一步缩小;不改变后端下发的布局半径、点击半径或三消规则。 - 物品进入底部物品栏时按同类型插入:如果物品栏已有同类物品,新物品插到该类型最后一个物品后面,后续物品整体后移;没有同类时追加到当前末尾。达到三件同类时,在飞入物品栏动画结束后,左侧和右侧同类物品向中间合成,三件一起消失,播放合成音效,不展示星星图标,后面的物品再向前补位。该动效只是前端表现层,后端和本地试玩仍负责权威插入、指定点击类型清除与补位后的槽位快照。 diff --git a/docs/【项目基线】当前产品与工程约束-2026-05-15.md b/docs/【项目基线】当前产品与工程约束-2026-05-15.md index 7b539e81..e19cc841 100644 --- a/docs/【项目基线】当前产品与工程约束-2026-05-15.md +++ b/docs/【项目基线】当前产品与工程约束-2026-05-15.md @@ -94,6 +94,8 @@ server-rs + Axum + SpacetimeDB 8. 图像输入通用 UI 统一走 `src/components/common/CreativeImageInputPanel.tsx`。外层页面持有业务状态,组件只承担上传卡、预览、参考图缩略图、AI 重绘开关、错误展示和提交按钮。 9. 发现页 `分类` 子频道的筛选必须打开独立 dialog / drawer / modal,至少支持玩法类型过滤与排序切换;筛选结果为空时显示空状态,不把筛选内容展开在当前列表下方。 10. “我的”页泥点、游戏时长、玩过三张统计卡只展示各自标签和值,内容居中且不换行,不在统计区底部展示“更新于”时间。 +11. RPG 等运行态的战斗飘字、血量变化和即时反馈必须在暗色、噪声高的场景背景上保持可读:使用高亮文字、深色描边、强阴影或小面积半透明底,不只依赖红/绿文字本身表达伤害或治疗。 +11. 平台亮色 UI 配色以陶泥儿主视觉为准:暖白 / 米杏底、陶土橙主按钮、深棕正文与浅杏边框;新增界面优先复用 `src/index.css` 的 `--platform-*` 主题变量和 `apps/admin-web/src/styles/admin.css` 的同系色值,不再引入粉红、蓝绿等独立主色方案。 ## 文案与编码 diff --git a/jenkins/Jenkinsfile.production-server-provision b/jenkins/Jenkinsfile.production-server-provision index 98757f54..3f3fa356 100644 --- a/jenkins/Jenkinsfile.production-server-provision +++ b/jenkins/Jenkinsfile.production-server-provision @@ -1,3 +1,27 @@ +def runWindowsPowerShell(String scriptName, String scriptBody) { + def scriptPath = ".jenkins-${scriptName}.ps1" + writeFile file: scriptPath, text: scriptBody, encoding: 'UTF-8' + bat label: "PowerShell ${scriptName}", script: """ +@echo off +setlocal +set "GENARRATIVE_POWERSHELL=%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" +if not exist "%GENARRATIVE_POWERSHELL%" ( + echo [jenkins-powershell] powershell.exe not found: %GENARRATIVE_POWERSHELL% + exit /b 1 +) +echo [jenkins-powershell] user: +whoami +echo [jenkins-powershell] workspace: %CD% +echo [jenkins-powershell] exe: %GENARRATIVE_POWERSHELL% +if not exist "%CD%\\${scriptPath}" ( + echo [jenkins-powershell] script not found: %CD%\\${scriptPath} + exit /b 1 +) +"%GENARRATIVE_POWERSHELL%" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "try { \$path = Join-Path (Get-Location).ProviderPath '${scriptPath}'; Write-Host '[jenkins-powershell] script:' \$path; \$text = [System.IO.File]::ReadAllText(\$path, [System.Text.Encoding]::UTF8); Write-Host '[jenkins-powershell] loaded bytes:' ([System.IO.File]::ReadAllBytes(\$path).Length); \$scriptBlock = [ScriptBlock]::Create(\$text); & \$scriptBlock; if (\$LASTEXITCODE -is [int] -and \$LASTEXITCODE -ne 0) { exit \$LASTEXITCODE } } catch { Write-Host '[jenkins-powershell] failed:' \$_.Exception.Message; if (\$_.ScriptStackTrace) { Write-Host \$_.ScriptStackTrace }; exit 1 }" +exit /b %ERRORLEVEL% +""" +} + pipeline { agent none @@ -22,8 +46,11 @@ pipeline { string(name: 'COMMIT_HASH', defaultValue: '', description: '部署脚本来源 commit') string(name: 'SERVER_NAME', defaultValue: 'genarrative.example.com', description: '证书主域名;也作为 Nginx server_name 的第一个域名') string(name: 'SERVER_ALIASES', defaultValue: '', description: '可选,额外 Nginx server_name,多个用空格或逗号分隔,例如 www.genarrative.world') - string(name: 'PROVISION_TOOLS_DIR', defaultValue: 'provision-tools', description: '构建机准备并上传到目标机工作区的工具包目录') - string(name: 'SPACETIME_DOWNLOAD_ROOT', defaultValue: 'https://github.com/clockworklabs/SpacetimeDB/releases/latest/download', description: '构建机下载 SpacetimeDB 官方安装产物的根地址;目标机不访问该地址') + string(name: 'PROVISION_DOWNLOADS_DIR', defaultValue: 'provision-tool-downloads', description: 'Windows 下载阶段暂存 SpacetimeDB/otelcol 安装包的工作区相对目录') + string(name: 'PROVISION_TOOLS_DIR', defaultValue: 'provision-tools', description: '目标机工作区内由已下载安装包生成的工具包目录') + string(name: 'PROVISION_DOWNLOAD_PROXY', defaultValue: '', description: '可选,Windows 下载 SpacetimeDB 和 otelcol-contrib 时使用的代理地址,例如 http://127.0.0.1:7890;留空不设置代理') + string(name: 'SPACETIME_DOWNLOAD_ROOT', defaultValue: 'https://github.com/clockworklabs/SpacetimeDB/releases/latest/download', description: 'Windows 下载 SpacetimeDB Linux release tarball 的根地址;目标机不访问该地址') + string(name: 'SPACETIME_TARGET_HOST', defaultValue: 'x86_64-unknown-linux-gnu', description: '目标机 SpacetimeDB 预编译包 host triple,development/release Linux amd64 使用默认值') string(name: 'SPACETIME_ROOT', defaultValue: '/stdb', description: 'SpacetimeDB root-dir') string(name: 'RELEASE_ROOT', defaultValue: '/opt/genarrative/releases', description: 'release 根目录') string(name: 'CURRENT_LINK', defaultValue: '/opt/genarrative/current', description: '当前版本软链接') @@ -34,13 +61,12 @@ pipeline { booleanParam(name: 'ENABLE_SERVICES', defaultValue: true, description: '启用并启动 spacetimedb 与 api-server systemd 服务') booleanParam(name: 'ENABLE_OTELCOL', defaultValue: true, description: '安装并启用本机 OpenTelemetry Collector;api-server 模板默认开启 OTLP,如需关闭请在 API_ENV_FILE 中将 GENARRATIVE_OTEL_ENABLED 改为 false') string(name: 'OTELCOL_VERSION', defaultValue: '0.151.0', description: 'otelcol-contrib 版本') - stashedFile 'OTELCOL_CONTRIB_ARCHIVE' } stages { stage('Prepare') { agent { - label 'linux && genarrative-build' + label 'windows' } steps { script { @@ -67,28 +93,33 @@ pipeline { if (!params.PROVISION_TOOLS_DIR?.trim()) { error('PROVISION_TOOLS_DIR 不能为空。') } - if (!(params.PROVISION_TOOLS_DIR.trim() ==~ /^[0-9A-Za-z._\/-]+$/) || params.PROVISION_TOOLS_DIR.startsWith('/') || params.PROVISION_TOOLS_DIR.contains('..')) { + if (!(params.PROVISION_TOOLS_DIR.trim() ==~ /^[0-9A-Za-z._\/-]+$/) || params.PROVISION_TOOLS_DIR.startsWith('/') || params.PROVISION_TOOLS_DIR.contains('..') || params.PROVISION_TOOLS_DIR.trim() == '.') { error("PROVISION_TOOLS_DIR 只能是工作区内的相对目录,不能包含绝对路径或连续点号: ${params.PROVISION_TOOLS_DIR}") } + if (!params.PROVISION_DOWNLOADS_DIR?.trim()) { + error('PROVISION_DOWNLOADS_DIR 不能为空。') + } + if (!(params.PROVISION_DOWNLOADS_DIR.trim() ==~ /^[0-9A-Za-z._\/-]+$/) || params.PROVISION_DOWNLOADS_DIR.startsWith('/') || params.PROVISION_DOWNLOADS_DIR.contains('..') || params.PROVISION_DOWNLOADS_DIR.trim() == '.') { + error("PROVISION_DOWNLOADS_DIR 只能是工作区内的相对目录,不能包含绝对路径或连续点号: ${params.PROVISION_DOWNLOADS_DIR}") + } + def provisionToolsDir = params.PROVISION_TOOLS_DIR.trim() + def provisionDownloadsDir = params.PROVISION_DOWNLOADS_DIR.trim() + if (provisionToolsDir == provisionDownloadsDir || provisionDownloadsDir.startsWith("${provisionToolsDir}/")) { + error("PROVISION_DOWNLOADS_DIR 不能等于或位于 PROVISION_TOOLS_DIR 内,否则目标机生成工具包时会删除下载缓存: ${provisionDownloadsDir}") + } + def provisionDownloadProxy = params.PROVISION_DOWNLOAD_PROXY?.trim() + if (provisionDownloadProxy && !(provisionDownloadProxy ==~ /^https?:\/\/\S+$/)) { + error("PROVISION_DOWNLOAD_PROXY 只能填写 http:// 或 https:// 开头的代理地址,当前值: ${params.PROVISION_DOWNLOAD_PROXY}") + } if (!(params.OTELCOL_VERSION?.trim() ==~ /^[0-9]+\.[0-9]+\.[0-9]+$/)) { error("OTELCOL_VERSION 格式应为 x.y.z: ${params.OTELCOL_VERSION}") } - def otelcolArchiveFilename = env.OTELCOL_CONTRIB_ARCHIVE_FILENAME?.trim() - def expectedOtelcolArchiveFilename = "otelcol-contrib_${params.OTELCOL_VERSION.trim()}_linux_amd64.tar.gz" - if (params.ENABLE_OTELCOL) { - if (!otelcolArchiveFilename) { - error("ENABLE_OTELCOL=true 时必须在 OTELCOL_CONTRIB_ARCHIVE 上传 ${expectedOtelcolArchiveFilename}。") - } - if (otelcolArchiveFilename != expectedOtelcolArchiveFilename) { - error("OTELCOL_CONTRIB_ARCHIVE 文件名必须是 ${expectedOtelcolArchiveFilename},当前上传: ${otelcolArchiveFilename}") - } - } - if (!params.ENABLE_OTELCOL && otelcolArchiveFilename) { - echo "ENABLE_OTELCOL=false,已上传的 OTELCOL_CONTRIB_ARCHIVE 将不会被安装。" - } - if (!params.SPACETIME_DOWNLOAD_ROOT?.trim()) { + if (!(params.SPACETIME_DOWNLOAD_ROOT?.trim() ==~ /^https?:\/\/\S+$/)) { error('SPACETIME_DOWNLOAD_ROOT 不能为空。') } + if (!(params.SPACETIME_TARGET_HOST?.trim() ==~ /^[0-9A-Za-z._-]+$/)) { + error("SPACETIME_TARGET_HOST 只能包含字母、数字、点号、下划线和短横线: ${params.SPACETIME_TARGET_HOST}") + } def nginxMode = params.NGINX_CONFIG_MODE?.trim() if (!(nginxMode in ['none', 'production-https', 'development-http'])) { error("NGINX_CONFIG_MODE 只能是 none、production-https 或 development-http,当前值: ${params.NGINX_CONFIG_MODE}") @@ -103,77 +134,258 @@ pipeline { } } - stage('Prepare Provision Tools') { + stage('Download Provision Tool Archives') { agent { - label 'linux && genarrative-build' + label 'windows' } steps { script { - def checkoutFromRemote = { String remoteUrl -> - checkout([ - $class: 'GitSCM', - branches: [[name: "*/${params.SOURCE_BRANCH}"]], - doGenerateSubmoduleConfigurations: false, - extensions: [ - [$class: 'CleanBeforeCheckout'], - [$class: 'CloneOption', shallow: true, depth: 1, noTags: true, timeout: 30, honorRefspec: true], - ], - userRemoteConfigs: [[url: remoteUrl, refspec: "+refs/heads/${params.SOURCE_BRANCH}:refs/remotes/origin/${params.SOURCE_BRANCH}"]], - ]) - } - try { - checkoutFromRemote(env.GIT_REMOTE_URL) - env.EFFECTIVE_GIT_REMOTE_URL = env.GIT_REMOTE_URL - } catch (error) { - echo "Git 主地址拉取失败: ${env.GIT_REMOTE_URL},改用备用地址: ${env.GIT_REMOTE_FALLBACK_URL}" - checkoutFromRemote(env.GIT_REMOTE_FALLBACK_URL) - env.EFFECTIVE_GIT_REMOTE_URL = env.GIT_REMOTE_FALLBACK_URL - } - } - sh ''' - bash <<'BASH' - set -euo pipefail - chmod +x scripts/jenkins-checkout-source.sh - SOURCE_BRANCH="${SOURCE_BRANCH:-master}" \ - COMMIT_HASH="${COMMIT_HASH:-}" \ - GIT_REMOTE_URL="${EFFECTIVE_GIT_REMOTE_URL:-${GIT_REMOTE_URL}}" \ - GIT_REMOTE_FALLBACK_URL="${GIT_REMOTE_FALLBACK_URL:-}" \ - SOURCE_COMMIT_FILE=".jenkins-source-commit" \ - scripts/jenkins-checkout-source.sh - # jenkins-checkout-source.sh 会 reset/clean 到目标 commit,前面的临时 chmod 可能被 Git mode 还原; - # 直接执行脚本前在二次 checkout 之后再补执行位,避免 Linux agent 报 Permission denied。 - chmod +x scripts/prepare-server-provision-tools.sh -BASH - ''' - script { - if (params.ENABLE_OTELCOL) { - echo "准备使用手动上传的 otelcol-contrib 包: ${env.OTELCOL_CONTRIB_ARCHIVE_FILENAME}" - sh 'bash -lc "rm -rf manual-provision-tool-upload && mkdir -p manual-provision-tool-upload"' - dir('manual-provision-tool-upload') { - unstash 'OTELCOL_CONTRIB_ARCHIVE' - } - env.OTELCOL_ARCHIVE_SOURCE = 'manual-provision-tool-upload/OTELCOL_CONTRIB_ARCHIVE' - } else { - env.OTELCOL_ARCHIVE_SOURCE = '' - } - } - sh ''' - bash <<'BASH' - set -euo pipefail + runWindowsPowerShell('server-provision-tool-downloads', ''' + $ErrorActionPreference = 'Stop' + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - PROVISION_TOOLS_DIR="${PROVISION_TOOLS_DIR:-provision-tools}" \ - OTELCOL_VERSION="${OTELCOL_VERSION:-0.151.0}" \ - PREPARE_OTELCOL="${ENABLE_OTELCOL:-true}" \ - OTELCOL_ARCHIVE_SOURCE="${OTELCOL_ARCHIVE_SOURCE:-}" \ - SPACETIME_DOWNLOAD_ROOT="${SPACETIME_DOWNLOAD_ROOT:-https://github.com/clockworklabs/SpacetimeDB/releases/latest/download}" \ - scripts/prepare-server-provision-tools.sh -BASH - ''' - script { - env.SOURCE_COMMIT = readFile('.jenkins-source-commit').trim() - echo "Provision 工具包已准备,源码 commit=${env.SOURCE_COMMIT}" + $downloadsDir = if ($env:PROVISION_DOWNLOADS_DIR) { $env:PROVISION_DOWNLOADS_DIR } else { 'provision-tool-downloads' } + $otelVersion = if ($env:OTELCOL_VERSION) { $env:OTELCOL_VERSION } else { '0.151.0' } + $prepareOtel = if ($env:ENABLE_OTELCOL) { $env:ENABLE_OTELCOL } else { 'true' } + $otelRoot = if ($env:OTELCOL_DOWNLOAD_ROOT) { $env:OTELCOL_DOWNLOAD_ROOT.TrimEnd('/') } else { 'https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download' } + $spacetimeDownloadRoot = if ($env:SPACETIME_DOWNLOAD_ROOT) { $env:SPACETIME_DOWNLOAD_ROOT.TrimEnd('/') } else { 'https://github.com/clockworklabs/SpacetimeDB/releases/latest/download' } + $spacetimeTargetHost = if ($env:SPACETIME_TARGET_HOST) { $env:SPACETIME_TARGET_HOST } else { 'x86_64-unknown-linux-gnu' } + $downloadProxy = if ($env:PROVISION_DOWNLOAD_PROXY) { $env:PROVISION_DOWNLOAD_PROXY } else { '' } + $workspace = (Get-Location).ProviderPath + if ([System.IO.Path]::IsPathRooted($downloadsDir)) { + throw "[prepare-provision-downloads] PROVISION_DOWNLOADS_DIR 只能是工作区内相对路径: ${downloadsDir}" + } + $downloadsDir = Join-Path $workspace $downloadsDir + Write-Host "[prepare-provision-downloads] Windows workspace: ${workspace}" + Write-Host "[prepare-provision-downloads] download dir: ${downloadsDir}" + + if (Test-Path -LiteralPath $downloadsDir) { + Write-Host "[prepare-provision-downloads] 复用已有下载目录: ${downloadsDir}" + } else { + New-Item -ItemType Directory -Force -Path $downloadsDir | Out-Null + Write-Host "[prepare-provision-downloads] 已创建下载目录: ${downloadsDir}" + } + + if ($downloadProxy) { + $env:HTTP_PROXY = $downloadProxy + $env:HTTPS_PROXY = $downloadProxy + $env:ALL_PROXY = $downloadProxy + Write-Host "[prepare-provision-downloads] 已配置 Windows 下载代理: $($downloadProxy -replace '://.*', '://***')" + } + + function Get-GithubReleaseAssetDigest { + param( + [Parameter(Mandatory=$true)][string]$Repository, + [Parameter(Mandatory=$true)][string]$ReleaseSelector, + [Parameter(Mandatory=$true)][string]$AssetName + ) + + $request = @{ + Uri = "https://api.github.com/repos/${Repository}/${ReleaseSelector}" + Headers = @{ + Accept = 'application/vnd.github+json' + 'User-Agent' = 'Genarrative-Server-Provision' + } + ErrorAction = 'Stop' + } + if ($downloadProxy) { + $request.Proxy = $downloadProxy + } + + Write-Host "[prepare-provision-downloads] 查询 GitHub digest: repo=${Repository} release=${ReleaseSelector} asset=${AssetName}" + $release = Invoke-RestMethod @request + $asset = $release.assets | Where-Object { $_.name -eq $AssetName } | Select-Object -First 1 + if (-not $asset) { + throw "[prepare-provision-downloads] GitHub release 未找到资产: ${Repository}/${AssetName}" + } + if (-not $asset.digest) { + throw "[prepare-provision-downloads] GitHub release 未返回 digest: ${Repository}/${AssetName}" + } + Write-Host "[prepare-provision-downloads] GitHub digest ${AssetName}: $($asset.digest)" + return $asset.digest + } + + function Get-FileDigest { + param( + [Parameter(Mandatory=$true)][string]$Path, + [Parameter(Mandatory=$true)][string]$Algorithm + ) + + $fileHash = Get-FileHash -Algorithm $Algorithm -LiteralPath $Path + return $fileHash.Hash.ToLowerInvariant() + } + + function Test-DownloadDigestMatch { + param( + [Parameter(Mandatory=$true)][string]$Path, + [Parameter(Mandatory=$true)][string]$ExpectedDigest + ) + + $parts = $ExpectedDigest.Split(':', 2) + if ($parts.Length -ne 2) { + throw "[prepare-provision-downloads] 无法解析 GitHub digest: ${ExpectedDigest}" + } + $algorithm = $parts[0].Trim().ToLowerInvariant() + $expectedHash = $parts[1].Trim().ToLowerInvariant() + if ($algorithm -ne 'sha256') { + throw "[prepare-provision-downloads] 暂不支持的 GitHub digest 算法: ${algorithm}" + } + $localHash = Get-FileDigest -Path $Path -Algorithm 'SHA256' + return $localHash -eq $expectedHash + } + + function Invoke-ProvisionDownload { + param( + [Parameter(Mandatory=$true)][string]$Label, + [Parameter(Mandatory=$true)][string]$Url, + [Parameter(Mandatory=$true)][string]$Output, + [string]$ExpectedDigest = '' + ) + + if ($ExpectedDigest) { + if (Test-Path -LiteralPath $Output) { + if (Test-DownloadDigestMatch -Path $Output -ExpectedDigest $ExpectedDigest) { + $existingItem = Get-Item -LiteralPath $Output + Write-Host "[prepare-provision-downloads] 已存在且校验一致,跳过下载: ${Label} bytes=$($existingItem.Length) path=${Output}" + return + } + Write-Host "[prepare-provision-downloads] 已存在但校验不一致,重新下载: ${Label} path=${Output}" + } + } + + Write-Host "[prepare-provision-downloads] 下载 ${Label}: ${Url}" + $tempOutput = "${Output}.download" + if (Test-Path -LiteralPath $tempOutput) { + $tempItem = Get-Item -LiteralPath $tempOutput + if ($ExpectedDigest -and $tempItem.Length -gt 0 -and (Test-DownloadDigestMatch -Path $tempOutput -ExpectedDigest $ExpectedDigest)) { + Move-Item -LiteralPath $tempOutput -Destination $Output -Force + $finalItem = Get-Item -LiteralPath $Output + Write-Host "[prepare-provision-downloads] 已复用校验通过的临时下载: ${Label} bytes=$($finalItem.Length) path=${Output}" + return + } + if ($tempItem.Length -gt 0) { + Write-Host "[prepare-provision-downloads] 发现未完成临时文件,后续尝试断点续传: ${Label} bytes=$($tempItem.Length) path=${tempOutput}" + } else { + Remove-Item -LiteralPath $tempOutput -Force + } + } + $curl = Get-Command curl.exe -ErrorAction SilentlyContinue + $maxAttempts = 8 + for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) { + $resumeBytes = 0 + if (Test-Path -LiteralPath $tempOutput) { + $resumeBytes = (Get-Item -LiteralPath $tempOutput).Length + } + try { + if ($curl) { + $arguments = @('-fL', '--retry', '3', '--retry-delay', '3', '--retry-all-errors', '--connect-timeout', '30', '--speed-time', '60', '--speed-limit', '1024') + if ($resumeBytes -gt 0) { + $arguments += @('-C', '-') + Write-Host "[prepare-provision-downloads] curl 断点续传 ${Label}: attempt=${attempt}/${maxAttempts} resumeBytes=${resumeBytes}" + } else { + Write-Host "[prepare-provision-downloads] curl 下载 ${Label}: attempt=${attempt}/${maxAttempts}" + } + $arguments += @('-o', $tempOutput) + if ($downloadProxy) { + $arguments += @('--proxy', $downloadProxy) + } + $arguments += $Url + & $curl.Source @arguments + $exitCode = $LASTEXITCODE + if ($exitCode -ne 0) { + $currentBytes = if (Test-Path -LiteralPath $tempOutput) { (Get-Item -LiteralPath $tempOutput).Length } else { 0 } + Write-Host "[prepare-provision-downloads] curl 下载未完成: ${Label}, attempt=${attempt}/${maxAttempts}, exit=${exitCode}, tempBytes=${currentBytes}" + if ($attempt -lt $maxAttempts) { + Start-Sleep -Seconds ([Math]::Min(30, 3 * $attempt)) + continue + } + throw "[prepare-provision-downloads] curl 下载失败: ${Label}, exit=${exitCode}, temp=${tempOutput}" + } + } else { + Write-Host "[prepare-provision-downloads] Invoke-WebRequest 下载 ${Label}: attempt=${attempt}/${maxAttempts}" + if ($resumeBytes -gt 0) { + Write-Host "[prepare-provision-downloads] Invoke-WebRequest 不支持断点续传,删除临时文件后重新下载: ${Label}, bytes=${resumeBytes}" + Remove-Item -LiteralPath $tempOutput -Force + } + $parameters = @{ + Uri = $Url + OutFile = $tempOutput + UseBasicParsing = $true + } + if ($downloadProxy) { + $parameters.Proxy = $downloadProxy + } + Invoke-WebRequest @parameters + } + } catch { + $currentBytes = if (Test-Path -LiteralPath $tempOutput) { (Get-Item -LiteralPath $tempOutput).Length } else { 0 } + Write-Host "[prepare-provision-downloads] 下载尝试失败: ${Label}, attempt=${attempt}/${maxAttempts}, tempBytes=${currentBytes}, error=$($_.Exception.Message)" + if ($attempt -lt $maxAttempts) { + Start-Sleep -Seconds ([Math]::Min(30, 3 * $attempt)) + continue + } + throw + } + + if (-not (Test-Path -LiteralPath $tempOutput)) { + throw "[prepare-provision-downloads] 下载未生成临时文件: ${tempOutput}" + } + $item = Get-Item -LiteralPath $tempOutput + if ($item.Length -le 0) { + if ($attempt -lt $maxAttempts) { + Write-Host "[prepare-provision-downloads] 下载结果为空,将重试: ${Label}" + Start-Sleep -Seconds ([Math]::Min(30, 3 * $attempt)) + continue + } + throw "[prepare-provision-downloads] 下载结果为空: ${tempOutput}" + } + if ($ExpectedDigest) { + if (-not (Test-DownloadDigestMatch -Path $tempOutput -ExpectedDigest $ExpectedDigest)) { + Write-Host "[prepare-provision-downloads] 下载结果校验未通过,将继续重试: ${Label}, attempt=${attempt}/${maxAttempts}, tempBytes=$($item.Length)" + if ($attempt -lt $maxAttempts) { + Remove-Item -LiteralPath $tempOutput -Force + Start-Sleep -Seconds ([Math]::Min(30, 3 * $attempt)) + continue + } + throw "[prepare-provision-downloads] 下载结果校验失败: ${Label}, temp=${tempOutput}" + } + } + Move-Item -LiteralPath $tempOutput -Destination $Output -Force + $finalItem = Get-Item -LiteralPath $Output + Write-Host "[prepare-provision-downloads] 已下载 ${Label}: bytes=$($finalItem.Length) path=${Output}" + return + } + throw "[prepare-provision-downloads] 下载重试耗尽: ${Label}" + } + + $spacetimeArchiveName = "spacetime-${spacetimeTargetHost}.tar.gz" + $spacetimeArchiveUrl = "${spacetimeDownloadRoot}/${spacetimeArchiveName}" + $spacetimeArchiveDigest = Get-GithubReleaseAssetDigest -Repository 'clockworklabs/SpacetimeDB' -ReleaseSelector 'releases/latest' -AssetName $spacetimeArchiveName + Invoke-ProvisionDownload -Label "SpacetimeDB release tarball ${spacetimeTargetHost}" -Url $spacetimeArchiveUrl -Output (Join-Path $downloadsDir $spacetimeArchiveName) -ExpectedDigest $spacetimeArchiveDigest + + if ($prepareOtel -eq 'true') { + $otelArchiveName = "otelcol-contrib_${otelVersion}_linux_amd64.tar.gz" + $otelUrl = "${otelRoot}/v${otelVersion}/${otelArchiveName}" + $otelDigest = Get-GithubReleaseAssetDigest -Repository 'open-telemetry/opentelemetry-collector-releases' -ReleaseSelector "releases/tags/v${otelVersion}" -AssetName $otelArchiveName + Invoke-ProvisionDownload -Label "otelcol-contrib ${otelVersion} linux amd64" -Url $otelUrl -Output (Join-Path $downloadsDir $otelArchiveName) -ExpectedDigest $otelDigest + } else { + Write-Host "[prepare-provision-downloads] ENABLE_OTELCOL=${prepareOtel},跳过 otelcol-contrib 下载。" + } + + $utf8NoBom = New-Object System.Text.UTF8Encoding $false + $manifest = @( + "spacetime release tarball ${spacetimeArchiveUrl}", + "spacetime target host ${spacetimeTargetHost}", + "otelcol-contrib ${otelVersion} prepare=${prepareOtel}" + ) + [System.IO.File]::WriteAllLines((Join-Path $downloadsDir 'DOWNLOADS-MANIFEST.txt'), $manifest, $utf8NoBom) + + Get-ChildItem -LiteralPath $downloadsDir | Sort-Object Name | ForEach-Object { + Write-Host "[prepare-provision-downloads] artifact $($_.Length) $($_.Name)" + } + ''') } - stash name: 'server-provision-tools', includes: "${params.PROVISION_TOOLS_DIR}/**", useDefaultExcludes: false + stash name: 'server-provision-tool-downloads', includes: "${params.PROVISION_DOWNLOADS_DIR}/**", useDefaultExcludes: false } } @@ -216,6 +428,10 @@ BASH scripts/jenkins-checkout-source.sh BASH ''' + script { + env.SOURCE_COMMIT = readFile('.jenkins-source-commit').trim() + echo "Provision 源码 commit=${env.SOURCE_COMMIT}" + } } } @@ -224,10 +440,20 @@ BASH label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}" } steps { - unstash 'server-provision-tools' + unstash 'server-provision-tool-downloads' sh ''' bash <<'BASH' set -euo pipefail + chmod +x scripts/prepare-server-provision-tools.sh + PROVISION_DOWNLOADS_DIR="${PROVISION_DOWNLOADS_DIR:-provision-tool-downloads}" \ + PROVISION_TOOLS_DIR="${PROVISION_TOOLS_DIR:-provision-tools}" \ + OTELCOL_VERSION="${OTELCOL_VERSION:-0.151.0}" \ + PREPARE_OTELCOL="${ENABLE_OTELCOL:-true}" \ + PROVISION_REQUIRE_LOCAL_DOWNLOADS="true" \ + SPACETIME_DOWNLOAD_ROOT="${SPACETIME_DOWNLOAD_ROOT:-https://github.com/clockworklabs/SpacetimeDB/releases/latest/download}" \ + SPACETIME_TARGET_HOST="${SPACETIME_TARGET_HOST:-x86_64-unknown-linux-gnu}" \ + scripts/prepare-server-provision-tools.sh + if [[ "${ENABLE_OTELCOL:-true}" == "true" ]]; then chmod +x "${PROVISION_TOOLS_DIR:-provision-tools}/otelcol-contrib" fi diff --git a/media/logo.png b/media/logo.png new file mode 100644 index 00000000..40a5c4fe Binary files /dev/null and b/media/logo.png differ diff --git a/package-lock.json b/package-lock.json index b30a634e..3756b11e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "vite": "^6.2.0" }, "devDependencies": { + "@colbymchenry/codegraph": "^0.8.0", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", "@types/node": "^22.14.0", @@ -301,6 +302,64 @@ "node": ">=6.9.0" } }, + "node_modules/@clack/core": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.3.1.tgz", + "integrity": "sha512-fT1qHVGAag4IEkrupZ6lRRbNCs1vS9P01KB/sG8zKgvUztbYtFBtQpjSITNwooDZ83tpsPzP0mRNs1/KVszCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.4.0.tgz", + "integrity": "sha512-S0My7XPGIgpRWMDG8uRqalbgT+a6FmCUdOW+HaIOVVpUPHOb7RrpvjTjiODadKp06fsrVDJZlIzc6yCTp4AnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@clack/core": "1.3.1", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@colbymchenry/codegraph": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@colbymchenry/codegraph/-/codegraph-0.8.0.tgz", + "integrity": "sha512-VvEdio2gP1i8mOgGWPPytzIB0UPoVO0kahjx7suFIuwuVdwKhFjSCrpCONtfcNCVhlQ3g+EMR5VbBKWXBf7F6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@clack/prompts": "^1.3.0", + "commander": "^14.0.2", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "jsonc-parser": "^3.3.1", + "node-sqlite3-wasm": "^0.8.30", + "picomatch": "^4.0.3", + "sisteransi": "^1.0.5", + "tree-sitter-wasms": "^0.1.11", + "web-tree-sitter": "^0.25.3" + }, + "bin": { + "codegraph": "dist/bin/codegraph.js" + }, + "engines": { + "node": ">=20.0.0 <25.0.0" + }, + "optionalDependencies": { + "better-sqlite3": "^12.4.1" + } + }, "node_modules/@dimforge/rapier3d-compat": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", @@ -2288,6 +2347,28 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/baseline-browser-mapping": { "version": "2.10.9", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.9.tgz", @@ -2299,6 +2380,46 @@ "node": ">=6.0.0" } }, + "node_modules/better-sqlite3": { + "version": "12.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz", + "integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", @@ -2354,6 +2475,32 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -2465,6 +2612,14 @@ "node": "*" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "optional": true + }, "node_modules/cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -2504,6 +2659,16 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2598,6 +2763,23 @@ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "dev": true }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-eql": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", @@ -2610,6 +2792,17 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2737,6 +2930,17 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.20.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", @@ -3083,6 +3287,17 @@ "node": ">=0.10.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3129,6 +3344,33 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -3173,6 +3415,14 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3276,6 +3526,14 @@ } } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3379,6 +3637,14 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -3557,6 +3823,28 @@ "node": ">= 6" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3608,6 +3896,14 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "optional": true + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3776,6 +4072,13 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4181,6 +4484,20 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -4193,6 +4510,25 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/mlly": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", @@ -4271,17 +4607,60 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-releases": { "version": "2.0.36", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==" }, + "node_modules/node-sqlite3-wasm": { + "version": "0.8.57", + "resolved": "https://registry.npmjs.org/node-sqlite3-wasm/-/node-sqlite3-wasm-0.8.57.tgz", + "integrity": "sha512-9sME3Agp6vqevHVgMvCV4PMsoTHjuwxjhiooNMiMjjPO3Ea3QbmyAbZn2H9Ko1rkTi2Oo8skv9Y3HvS+rSMcMA==", + "dev": true, + "license": "MIT" + }, "node_modules/nwsapi": { "version": "2.2.23", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", @@ -4436,7 +4815,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "peer": true, "engines": { "node": ">=12" }, @@ -4504,6 +4882,35 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4566,6 +4973,18 @@ "url": "https://github.com/sponsors/lupomontero" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4618,6 +5037,34 @@ } ] }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -4653,6 +5100,22 @@ "node": ">=0.10.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4790,6 +5253,28 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -4854,6 +5339,62 @@ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -4883,6 +5424,17 @@ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -4967,6 +5519,38 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5057,6 +5641,16 @@ "node": ">=14" } }, + "node_modules/tree-sitter-wasms": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/tree-sitter-wasms/-/tree-sitter-wasms-0.1.13.tgz", + "integrity": "sha512-wT+cR6DwaIz80/vho3AvSF0N4txuNx/5bcRKoXouOfClpxh/qqrF4URNLQXbbt8MaAxeksZcZd1j8gcGjc+QxQ==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "tree-sitter-wasms": "^0.1.11" + } + }, "node_modules/ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", @@ -5094,6 +5688,20 @@ "fsevents": "~2.3.3" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5210,6 +5818,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/vite": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", @@ -6746,6 +7362,21 @@ "node": ">=14" } }, + "node_modules/web-tree-sitter": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.25.10.tgz", + "integrity": "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/emscripten": "^1.40.0" + }, + "peerDependenciesMeta": { + "@types/emscripten": { + "optional": true + } + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -7194,6 +7825,47 @@ "@babel/helper-validator-identifier": "^7.28.5" } }, + "@clack/core": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.3.1.tgz", + "integrity": "sha512-fT1qHVGAag4IEkrupZ6lRRbNCs1vS9P01KB/sG8zKgvUztbYtFBtQpjSITNwooDZ83tpsPzP0mRNs1/KVszCRA==", + "dev": true, + "requires": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + } + }, + "@clack/prompts": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.4.0.tgz", + "integrity": "sha512-S0My7XPGIgpRWMDG8uRqalbgT+a6FmCUdOW+HaIOVVpUPHOb7RrpvjTjiODadKp06fsrVDJZlIzc6yCTp4AnxA==", + "dev": true, + "requires": { + "@clack/core": "1.3.1", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + } + }, + "@colbymchenry/codegraph": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@colbymchenry/codegraph/-/codegraph-0.8.0.tgz", + "integrity": "sha512-VvEdio2gP1i8mOgGWPPytzIB0UPoVO0kahjx7suFIuwuVdwKhFjSCrpCONtfcNCVhlQ3g+EMR5VbBKWXBf7F6w==", + "dev": true, + "requires": { + "@clack/prompts": "^1.3.0", + "better-sqlite3": "^12.4.1", + "commander": "^14.0.2", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "jsonc-parser": "^3.3.1", + "node-sqlite3-wasm": "^0.8.30", + "picomatch": "^4.0.3", + "sisteransi": "^1.0.5", + "tree-sitter-wasms": "^0.1.11", + "web-tree-sitter": "^0.25.3" + } + }, "@dimforge/rapier3d-compat": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", @@ -8377,11 +9049,51 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "optional": true + }, "baseline-browser-mapping": { "version": "2.10.9", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.9.tgz", "integrity": "sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg==" }, + "better-sqlite3": { + "version": "12.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz", + "integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "optional": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "brace-expansion": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", @@ -8414,6 +9126,17 @@ "update-browserslist-db": "^1.2.0" } }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "optional": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -8485,6 +9208,13 @@ "get-func-name": "^2.0.2" } }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "optional": true + }, "cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -8517,6 +9247,12 @@ "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -8590,6 +9326,16 @@ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "dev": true }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "optional": true, + "requires": { + "mimic-response": "^3.1.0" + } + }, "deep-eql": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", @@ -8599,6 +9345,13 @@ "type-detect": "^4.0.0" } }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "optional": true + }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -8692,6 +9445,16 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "optional": true, + "requires": { + "once": "^1.4.0" + } + }, "enhanced-resolve": { "version": "5.20.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", @@ -8932,6 +9695,13 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "optional": true + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -8974,6 +9744,30 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true + }, + "fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "requires": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "requires": { + "fast-string-width": "^3.0.2" + } + }, "fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -9004,6 +9798,13 @@ "flat-cache": "^3.0.4" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -9069,6 +9870,13 @@ "tslib": "^2.4.0" } }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "optional": true + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -9140,6 +9948,13 @@ "resolve-pkg-maps": "^1.0.0" } }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "optional": true + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -9263,6 +10078,13 @@ "debug": "4" } }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "optional": true + }, "ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -9301,6 +10123,13 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "optional": true + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -9424,6 +10253,12 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" }, + "jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true + }, "keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -9637,6 +10472,13 @@ "mime-db": "1.52.0" } }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "optional": true + }, "minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -9646,6 +10488,20 @@ "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "optional": true + }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "optional": true + }, "mlly": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", @@ -9698,17 +10554,49 @@ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==" }, + "napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "optional": true + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "dev": true, + "optional": true, + "requires": { + "semver": "^7.3.5" + }, + "dependencies": { + "semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "optional": true + } + } + }, "node-releases": { "version": "2.0.36", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==" }, + "node-sqlite3-wasm": { + "version": "0.8.57", + "resolved": "https://registry.npmjs.org/node-sqlite3-wasm/-/node-sqlite3-wasm-0.8.57.tgz", + "integrity": "sha512-9sME3Agp6vqevHVgMvCV4PMsoTHjuwxjhiooNMiMjjPO3Ea3QbmyAbZn2H9Ko1rkTi2Oo8skv9Y3HvS+rSMcMA==", + "dev": true + }, "nwsapi": { "version": "2.2.23", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", @@ -9822,8 +10710,7 @@ "picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "peer": true + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==" }, "pkg-types": { "version": "1.3.1", @@ -9866,6 +10753,27 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + } + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -9906,6 +10814,17 @@ "punycode": "^2.3.1" } }, + "pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "optional": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -9934,6 +10853,28 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "optional": true + } + } + }, "react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -9960,6 +10901,18 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==" }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -10052,6 +11005,13 @@ "queue-microtask": "^1.2.2" } }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "optional": true + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -10103,6 +11063,31 @@ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "optional": true + }, + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "optional": true, + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -10126,6 +11111,16 @@ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -10184,6 +11179,33 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==" }, + "tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "optional": true, + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -10252,6 +11274,15 @@ "punycode": "^2.3.0" } }, + "tree-sitter-wasms": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/tree-sitter-wasms/-/tree-sitter-wasms-0.1.13.tgz", + "integrity": "sha512-wT+cR6DwaIz80/vho3AvSF0N4txuNx/5bcRKoXouOfClpxh/qqrF4URNLQXbbt8MaAxeksZcZd1j8gcGjc+QxQ==", + "dev": true, + "requires": { + "tree-sitter-wasms": "^0.1.11" + } + }, "ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", @@ -10276,6 +11307,16 @@ "get-tsconfig": "^4.7.5" } }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -10350,6 +11391,13 @@ "requires-port": "^1.0.0" } }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "optional": true + }, "vite": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", @@ -11025,6 +12073,13 @@ "xml-name-validator": "^4.0.0" } }, + "web-tree-sitter": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.25.10.tgz", + "integrity": "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA==", + "dev": true, + "requires": {} + }, "webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/package.json b/package.json index 4f65c0b6..9a6a6da8 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,11 @@ "check:data": "node scripts/run-tsx.cjs scripts/validate-content.ts", "check:overrides": "node scripts/run-tsx.cjs scripts/validate-overrides.ts", "check:smoke": "node scripts/run-tsx.cjs scripts/smoke-content.ts", - "check:content": "npm run check:data && npm run check:overrides && npm run check:smoke" + "check:content": "npm run check:data && npm run check:overrides && npm run check:smoke", + "codegraph:init": "codegraph init -i .", + "codegraph:index": "codegraph index .", + "codegraph:sync": "codegraph sync .", + "codegraph:status": "codegraph status ." }, "dependencies": { "@tailwindcss/vite": "^4.1.14", @@ -73,6 +77,7 @@ "vite": "^6.2.0" }, "devDependencies": { + "@colbymchenry/codegraph": "^0.8.0", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", "@types/node": "^22.14.0", diff --git a/packages/shared/src/contracts/index.ts b/packages/shared/src/contracts/index.ts index 105b187b..8f8b20e8 100644 --- a/packages/shared/src/contracts/index.ts +++ b/packages/shared/src/contracts/index.ts @@ -1,6 +1,7 @@ export type * from './creativeAgent'; export type * from './creationAudio'; export type * from './hyper3d'; +export type * from './jumpHop'; export type * from './puzzleCreativeTemplate'; export type * from './visualNovel'; export type * from './barkBattle'; diff --git a/packages/shared/src/contracts/jumpHop.ts b/packages/shared/src/contracts/jumpHop.ts new file mode 100644 index 00000000..856e04bf --- /dev/null +++ b/packages/shared/src/contracts/jumpHop.ts @@ -0,0 +1,262 @@ +export type JumpHopDifficulty = 'easy' | 'standard' | 'advanced' | 'challenge'; + +export type JumpHopStylePreset = + | 'minimal-blocks' + | 'paper-toy' + | 'neon-glass' + | 'forest-stone' + | 'future-metal' + | 'custom'; + +export type JumpHopGenerationStatus = + | 'draft' + | 'generating' + | 'ready' + | 'failed'; + +export type JumpHopTileType = + | 'start' + | 'normal' + | 'target' + | 'finish' + | 'bonus' + | 'accent'; + +export type JumpHopActionType = + | 'compile-draft' + | 'regenerate-character' + | 'regenerate-tiles' + | 'update-work-meta' + | 'update-difficulty'; + +export type JumpHopRunStatus = 'playing' | 'failed' | 'cleared'; + +export type JumpHopJumpResult = 'miss' | 'hit' | 'perfect' | 'finish'; + +export interface JumpHopWorkspaceCreateRequest { + templateId: string; + workTitle: string; + workDescription: string; + themeTags: string[]; + difficulty: JumpHopDifficulty; + stylePreset: JumpHopStylePreset; + characterPrompt: string; + tilePrompt: string; + endMoodPrompt?: string | null; +} + +export interface JumpHopActionRequest { + actionType: JumpHopActionType; + workTitle?: string | null; + workDescription?: string | null; + themeTags?: string[] | null; + difficulty?: JumpHopDifficulty | null; + stylePreset?: JumpHopStylePreset | null; + characterPrompt?: string | null; + tilePrompt?: string | null; + endMoodPrompt?: string | null; +} + +export interface JumpHopCharacterAsset { + assetId: string; + imageSrc: string; + imageObjectKey: string; + assetObjectId: string; + generationProvider: string; + prompt: string; + width: number; + height: number; +} + +export interface JumpHopTileAsset { + tileType: JumpHopTileType; + imageSrc: string; + imageObjectKey: string; + assetObjectId: string; + sourceAtlasCell: string; + visualWidth: number; + visualHeight: number; + topSurfaceRadius: number; + landingRadius: number; +} + +export interface JumpHopScoring { + chargeToDistanceRatio: number; + maxChargeMs: number; + hitBonus: number; + perfectBonus: number; +} + +export interface JumpHopPlatform { + platformId: string; + tileType: JumpHopTileType; + x: number; + y: number; + width: number; + height: number; + landingRadius: number; + perfectRadius: number; + scoreValue: number; +} + +export interface JumpHopPath { + seed: string; + difficulty: JumpHopDifficulty; + platforms: JumpHopPlatform[]; + finishIndex: number; + cameraPreset: string; + scoring: JumpHopScoring; +} + +export interface JumpHopLastJump { + chargeMs: number; + jumpDistance: number; + targetPlatformIndex: number; + landedX: number; + landedY: number; + result: JumpHopJumpResult; +} + +export interface JumpHopDraftResponse { + templateId: string; + templateName: string; + profileId: string | null; + workTitle: string; + workDescription: string; + themeTags: string[]; + difficulty: JumpHopDifficulty; + stylePreset: JumpHopStylePreset; + characterPrompt: string; + tilePrompt: string; + endMoodPrompt: string | null; + characterAsset: JumpHopCharacterAsset | null; + tileAtlasAsset: JumpHopCharacterAsset | null; + tileAssets: JumpHopTileAsset[]; + path: JumpHopPath | null; + coverComposite: string | null; + generationStatus: JumpHopGenerationStatus; +} + +export interface JumpHopSessionSnapshotResponse { + sessionId: string; + ownerUserId: string; + status: JumpHopGenerationStatus; + draft: JumpHopDraftResponse | null; + createdAt: string; + updatedAt: string; +} + +export interface JumpHopSessionResponse { + session: JumpHopSessionSnapshotResponse; +} + +export interface JumpHopActionResponse { + actionType: JumpHopActionType; + session: JumpHopSessionSnapshotResponse; + work: JumpHopWorkProfileResponse | null; +} + +export interface JumpHopWorkSummaryResponse { + runtimeKind: 'jump-hop'; + workId: string; + profileId: string; + ownerUserId: string; + sourceSessionId: string | null; + workTitle: string; + workDescription: string; + themeTags: string[]; + difficulty: JumpHopDifficulty; + stylePreset: JumpHopStylePreset; + coverImageSrc: string | null; + publicationStatus: string; + playCount: number; + updatedAt: string; + publishedAt: string | null; + publishReady: boolean; + generationStatus: JumpHopGenerationStatus; +} + +export interface JumpHopWorkProfileResponse { + summary: JumpHopWorkSummaryResponse; + draft: JumpHopDraftResponse; + path: JumpHopPath; + characterAsset: JumpHopCharacterAsset; + tileAtlasAsset: JumpHopCharacterAsset; + tileAssets: JumpHopTileAsset[]; +} + +export interface JumpHopWorksResponse { + items: JumpHopWorkSummaryResponse[]; +} + +export interface JumpHopWorkDetailResponse { + item: JumpHopWorkProfileResponse; +} + +export interface JumpHopWorkMutationResponse { + item: JumpHopWorkProfileResponse; +} + +export interface JumpHopGalleryCardResponse { + publicWorkCode: string; + workId: string; + profileId: string; + ownerUserId: string; + authorDisplayName: string; + workTitle: string; + workDescription: string; + coverImageSrc: string | null; + themeTags: string[]; + difficulty: JumpHopDifficulty; + stylePreset: JumpHopStylePreset; + publicationStatus: string; + playCount: number; + updatedAt: string; + publishedAt: string | null; + generationStatus: JumpHopGenerationStatus; +} + +export interface JumpHopGalleryResponse { + items: JumpHopGalleryCardResponse[]; + hasMore: boolean; + nextCursor: string | null; +} + +export interface JumpHopGalleryDetailResponse { + item: JumpHopWorkProfileResponse; +} + +export interface JumpHopRuntimeRunSnapshotResponse { + runId: string; + profileId: string; + ownerUserId: string; + status: JumpHopRunStatus; + currentPlatformIndex: number; + score: number; + combo: number; + path: JumpHopPath; + lastJump: JumpHopLastJump | null; + startedAtMs: number; + finishedAtMs: number | null; +} + +export interface JumpHopRunResponse { + run: JumpHopRuntimeRunSnapshotResponse; +} + +export interface JumpHopStartRunRequest { + profileId: string; +} + +export interface JumpHopJumpRequest { + chargeMs: number; + clientEventId: string; +} + +export interface JumpHopRestartRunRequest { + clientActionId: string; +} + +export interface JumpHopJumpResponse { + run: JumpHopRuntimeRunSnapshotResponse; +} diff --git a/packages/shared/src/contracts/match3dWorks.ts b/packages/shared/src/contracts/match3dWorks.ts index f54ac624..146ca360 100644 --- a/packages/shared/src/contracts/match3dWorks.ts +++ b/packages/shared/src/contracts/match3dWorks.ts @@ -19,8 +19,17 @@ export type Match3DGeneratedItemSize = '大' | '中' | '小' | string; export interface Match3DGeneratedBackgroundAsset { prompt: string; + levelScenePrompt?: string | null; + levelSceneImageSrc?: string | null; + levelSceneImageObjectKey?: string | null; imageSrc?: string | null; imageObjectKey?: string | null; + uiSpritesheetPrompt?: string | null; + uiSpritesheetImageSrc?: string | null; + uiSpritesheetImageObjectKey?: string | null; + itemSpritesheetPrompt?: string | null; + itemSpritesheetImageSrc?: string | null; + itemSpritesheetImageObjectKey?: string | null; containerPrompt?: string | null; containerImageSrc?: string | null; containerImageObjectKey?: string | null; diff --git a/packages/shared/src/contracts/puzzleAgentActions.ts b/packages/shared/src/contracts/puzzleAgentActions.ts index 9e6d2cb3..501b8cc4 100644 --- a/packages/shared/src/contracts/puzzleAgentActions.ts +++ b/packages/shared/src/contracts/puzzleAgentActions.ts @@ -52,6 +52,8 @@ export type PuzzleAgentActionRequest = pictureDescription?: string; referenceImageSrc?: string | null; referenceImageSrcs?: string[]; + referenceImageAssetObjectId?: string | null; + referenceImageAssetObjectIds?: string[]; imageModel?: string | null; aiRedraw?: boolean; } @@ -63,6 +65,8 @@ export type PuzzleAgentActionRequest = pictureDescription?: string; referenceImageSrc?: string | null; referenceImageSrcs?: string[]; + referenceImageAssetObjectId?: string | null; + referenceImageAssetObjectIds?: string[]; imageModel?: string | null; aiRedraw?: boolean; candidateCount?: number; @@ -73,6 +77,8 @@ export type PuzzleAgentActionRequest = promptText?: string | null; referenceImageSrc?: string | null; referenceImageSrcs?: string[]; + referenceImageAssetObjectId?: string | null; + referenceImageAssetObjectIds?: string[]; imageModel?: string | null; aiRedraw?: boolean; candidateCount?: number; diff --git a/packages/shared/src/contracts/puzzleAgentDraft.ts b/packages/shared/src/contracts/puzzleAgentDraft.ts index 228065f0..38fa22fb 100644 --- a/packages/shared/src/contracts/puzzleAgentDraft.ts +++ b/packages/shared/src/contracts/puzzleAgentDraft.ts @@ -51,6 +51,12 @@ export interface PuzzleDraftLevel { uiBackgroundPrompt?: string | null; uiBackgroundImageSrc?: string | null; uiBackgroundImageObjectKey?: string | null; + levelSceneImageSrc?: string | null; + levelSceneImageObjectKey?: string | null; + uiSpritesheetImageSrc?: string | null; + uiSpritesheetImageObjectKey?: string | null; + levelBackgroundImageSrc?: string | null; + levelBackgroundImageObjectKey?: string | null; backgroundMusic?: CreationAudioAsset | null; candidates: PuzzleGeneratedImageCandidate[]; selectedCandidateId: string | null; diff --git a/packages/shared/src/contracts/puzzleAgentSession.ts b/packages/shared/src/contracts/puzzleAgentSession.ts index f216f263..2859bb73 100644 --- a/packages/shared/src/contracts/puzzleAgentSession.ts +++ b/packages/shared/src/contracts/puzzleAgentSession.ts @@ -51,6 +51,8 @@ export interface CreatePuzzleAgentSessionRequest { pictureDescription?: string; referenceImageSrc?: string | null; referenceImageSrcs?: string[]; + referenceImageAssetObjectId?: string | null; + referenceImageAssetObjectIds?: string[]; imageModel?: string | null; aiRedraw?: boolean; } diff --git a/packages/shared/src/contracts/puzzleRuntimeSession.ts b/packages/shared/src/contracts/puzzleRuntimeSession.ts index 10f3532a..5e705782 100644 --- a/packages/shared/src/contracts/puzzleRuntimeSession.ts +++ b/packages/shared/src/contracts/puzzleRuntimeSession.ts @@ -59,6 +59,10 @@ export interface PuzzleRuntimeLevelSnapshot { coverImageSrc: string | null; uiBackgroundImageSrc?: string | null; uiBackgroundImageObjectKey?: string | null; + levelBackgroundImageSrc?: string | null; + levelBackgroundImageObjectKey?: string | null; + uiSpritesheetImageSrc?: string | null; + uiSpritesheetImageObjectKey?: string | null; backgroundMusic?: CreationAudioAsset | null; board: PuzzleBoardSnapshot; status: PuzzleRuntimeLevelStatus; diff --git a/scripts/check-spacetime-schema-guard.mjs b/scripts/check-spacetime-schema-guard.mjs index 72cdfc34..6f72ac8d 100644 --- a/scripts/check-spacetime-schema-guard.mjs +++ b/scripts/check-spacetime-schema-guard.mjs @@ -474,11 +474,11 @@ function loadBaseSources(baseRef) { } function getChangedFiles(baseRef) { - const diffOutput = tryGit(['diff', '--name-only', baseRef, '--']) ?? ''; + const diffOutput = tryGit(['diff', '--name-only', '-z', baseRef, '--']) ?? ''; const untrackedOutput = - tryGit(['ls-files', '--others', '--exclude-standard', moduleSrcRoot]) ?? ''; + tryGit(['ls-files', '--others', '--exclude-standard', '-z', moduleSrcRoot]) ?? ''; return new Set( - [...diffOutput.split(/\r?\n/u), ...untrackedOutput.split(/\r?\n/u)] + [...diffOutput.split(/\u0000/u), ...untrackedOutput.split(/\u0000/u)] .map(normalizePath) .filter(Boolean), ); diff --git a/scripts/deploy/production-api-deploy.sh b/scripts/deploy/production-api-deploy.sh index 55601b18..992604cd 100644 --- a/scripts/deploy/production-api-deploy.sh +++ b/scripts/deploy/production-api-deploy.sh @@ -120,6 +120,115 @@ PY fi } +read_env_value() { + local file_path="$1" + local key="$2" + + if [[ ! -f "${file_path}" ]]; then + return 0 + fi + + local python_script=' +import sys +from pathlib import Path + +path = Path(sys.argv[1]) +key = sys.argv[2] +if not path.exists(): + raise SystemExit(0) +for raw_line in path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + current_key, value = line.split("=", 1) + if current_key == key: + value = value.strip() + if len(value) >= 2 and value[0] == value[-1] and value[0] in ("\"", "'\''"): + value = value[1:-1] + print(value) + raise SystemExit(0) +' + + if [[ -r "${file_path}" ]]; then + python3 -c "${python_script}" "${file_path}" "${key}" + else + if ! sudo -n true >/dev/null 2>&1; then + echo "[production-api-deploy] 当前用户无权读取 ${file_path},且 sudo -n 不可用;无法检查运行态环境变量。" >&2 + exit 1 + fi + sudo -n python3 -c "${python_script}" "${file_path}" "${key}" + fi +} + +ensure_env_value() { + local file_path="$1" + local key="$2" + local default_value="$3" + local current_value + + current_value="$(read_env_value "${file_path}" "${key}")" + if [[ -n "${current_value}" ]]; then + return + fi + + echo "[production-api-deploy] 补齐 api-server 环境变量: ${key} -> ${file_path}" + write_env_value "${file_path}" "${key}" "${default_value}" +} + +run_privileged() { + if [[ "$(id -u)" -eq 0 ]]; then + "$@" + return + fi + if ! sudo -n true >/dev/null 2>&1; then + echo "[production-api-deploy] 当前用户不是 root,且 sudo -n 不可用;无法执行: $*" >&2 + exit 1 + fi + sudo -n "$@" +} + +ensure_runtime_dir() { + local path="$1" + local mode="$2" + + if [[ -z "${path}" ]]; then + return + fi + if [[ "${path}" != /* ]]; then + echo "[production-api-deploy] 运行态目录必须使用绝对路径,避免写入只读发布目录: ${path}" >&2 + exit 1 + fi + + echo "[production-api-deploy] 确保运行态目录可写: ${path}" + run_privileged install -d -o genarrative -g genarrative -m "${mode}" "${path}" +} + +ensure_runtime_env_and_dirs() { + local api_env_file="$1" + local tracking_enabled tracking_outbox_dir auth_store_path auth_store_dir + + # 旧生产环境文件会被 server-provision 保留,不一定包含新增的运行态写入路径。 + # 发布前只补缺省值,不覆盖线上已经定制过的目录或开关。 + ensure_env_value "${api_env_file}" "GENARRATIVE_TRACKING_OUTBOX_ENABLED" "true" + ensure_env_value "${api_env_file}" "GENARRATIVE_TRACKING_OUTBOX_DIR" "/var/lib/genarrative/tracking-outbox" + ensure_env_value "${api_env_file}" "GENARRATIVE_TRACKING_OUTBOX_BATCH_SIZE" "500" + ensure_env_value "${api_env_file}" "GENARRATIVE_TRACKING_OUTBOX_FLUSH_INTERVAL_MS" "1000" + ensure_env_value "${api_env_file}" "GENARRATIVE_TRACKING_OUTBOX_MAX_BYTES" "268435456" + ensure_env_value "${api_env_file}" "GENARRATIVE_AUTH_STORE_PATH" "/var/lib/genarrative/auth/auth-store.json" + + tracking_enabled="$(read_env_value "${api_env_file}" "GENARRATIVE_TRACKING_OUTBOX_ENABLED")" + tracking_outbox_dir="$(read_env_value "${api_env_file}" "GENARRATIVE_TRACKING_OUTBOX_DIR")" + if [[ "$(printf "%s" "${tracking_enabled}" | tr '[:upper:]' '[:lower:]')" != "false" ]]; then + ensure_runtime_dir "${tracking_outbox_dir}" "0750" + fi + + auth_store_path="$(read_env_value "${api_env_file}" "GENARRATIVE_AUTH_STORE_PATH")" + if [[ -n "${auth_store_path}" ]]; then + auth_store_dir="$(dirname "${auth_store_path}")" + ensure_runtime_dir "${auth_store_dir}" "0750" + fi +} + SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" SOURCE_DIR="" VERSION="" @@ -243,6 +352,8 @@ if [[ -n "${SPACETIME_SERVER_URL}" ]]; then write_env_value "${API_ENV_FILE}" "GENARRATIVE_SPACETIME_SERVER_URL" "${SPACETIME_SERVER_URL}" fi +ensure_runtime_env_and_dirs "${API_ENV_FILE}" + mkdir -p "$(dirname "${CURRENT_LINK}")" ln -sfn "${RELEASE_DIR}" "${CURRENT_LINK}" diff --git a/scripts/generate-bark-battle-assets.mjs b/scripts/generate-bark-battle-assets.mjs index 1a605519..f7e01211 100644 --- a/scripts/generate-bark-battle-assets.mjs +++ b/scripts/generate-bark-battle-assets.mjs @@ -1,4 +1,4 @@ -import { Buffer } from 'node:buffer'; +import { Buffer } from 'node:buffer'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import path from 'node:path'; @@ -101,7 +101,7 @@ const onlyIds = process.argv .filter(Boolean); const templates = rawTemplates.filter((template) => !onlyIds.length || onlyIds.includes(template.id)); const dryRun = args.has('--dry-run') || !args.has('--live'); -const requests = templates.map((template) => ({ id: template.id, title: template.title, body: { model: 'gpt-image-2-all', prompt: template.prompt, n: 1, size: '1024x1024' } })); +const requests = templates.map((template) => ({ id: template.id, title: template.title, body: { model: 'gpt-image-2', prompt: template.prompt, n: 1, size: '1024x1024' } })); if (dryRun) { console.log(JSON.stringify({ mode: 'dry-run', outDir, count: requests.length, requests }, null, 2)); process.exit(0); diff --git a/scripts/generate-child-motion-demo-assets.mjs b/scripts/generate-child-motion-demo-assets.mjs index e3a81fbb..f39f6928 100644 --- a/scripts/generate-child-motion-demo-assets.mjs +++ b/scripts/generate-child-motion-demo-assets.mjs @@ -772,6 +772,12 @@ function buildVectorEngineImagesGenerationUrl(baseUrl) { : `${baseUrl}/v1/images/generations`; } +function buildVectorEngineImagesEditUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/edits` + : `${baseUrl}/v1/images/edits`; +} + function collectStringsByKey(value, targetKey, output) { if (Array.isArray(value)) { value.forEach((entry) => collectStringsByKey(entry, targetKey, output)); @@ -828,28 +834,41 @@ function inferExtensionFromBytes(bytes, preferredPath) { return path.extname(preferredPath).replace(/^\./u, '') || 'png'; } -function toDataUrl(filePath) { +function mimeFromExtension(extension) { + if (extension === 'jpg' || extension === 'jpeg') { + return 'image/jpeg'; + } + if (extension === 'webp') { + return 'image/webp'; + } + return 'image/png'; +} + +function readReferenceImage(filePath) { if (!existsSync(filePath)) { return null; } const bytes = readFileSync(filePath); const extension = inferExtensionFromBytes(bytes, filePath); - const mime = extension === 'jpg' ? 'image/jpeg' : `image/${extension}`; - return `data:${mime};base64,${bytes.toString('base64')}`; + return { + fileName: path.basename(filePath).replace(/"/gu, '_'), + mimeType: mimeFromExtension(extension), + bytes, + }; } function pushReferenceImage(body, filePath) { - const reference = toDataUrl(filePath); + const reference = readReferenceImage(filePath); if (!reference) { return false; } - body.image = [...(body.image || []), reference]; + body.referenceImages = [...(body.referenceImages || []), reference]; return true; } function buildRequestBody(asset, size) { const body = { - model: 'gpt-image-2-all', + model: 'gpt-image-2', prompt: asset.prompt, n: 1, size: size || asset.size, @@ -1624,18 +1643,49 @@ async function generateAsset(asset, env, size, force) { }; } - const requestBody = buildRequestBody(asset, size); + const { referenceImages = [], ...requestBody } = buildRequestBody(asset, size); + const hasReferenceImages = referenceImages.length > 0; + const requestOptions = hasReferenceImages + ? (() => { + const formData = new FormData(); + formData.set('model', requestBody.model); + formData.set('prompt', requestBody.prompt); + formData.set('n', String(requestBody.n)); + formData.set('size', requestBody.size); + for (const referenceImage of referenceImages) { + formData.append( + 'image', + new Blob([referenceImage.bytes], { type: referenceImage.mimeType }), + referenceImage.fileName, + ); + } + return { + url: buildVectorEngineImagesEditUrl(env.baseUrl), + options: { + method: 'POST', + headers: { + Authorization: `Bearer ${env.apiKey}`, + Accept: 'application/json', + }, + body: formData, + }, + }; + })() + : { + url: buildVectorEngineImagesGenerationUrl(env.baseUrl), + options: { + method: 'POST', + headers: { + Authorization: `Bearer ${env.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }, + }; const payloadText = await fetchWithTimeout( - buildVectorEngineImagesGenerationUrl(env.baseUrl), - { - method: 'POST', - headers: { - Authorization: `Bearer ${env.apiKey}`, - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBody), - }, + requestOptions.url, + requestOptions.options, env.timeoutMs, ); @@ -1687,7 +1737,7 @@ async function generateAsset(asset, env, size, force) { size: requestBody.size, extension: actualExtension, source: urls[0] ? 'url' : 'b64_json', - usedReferenceImage: Boolean(requestBody.image), + usedReferenceImage: hasReferenceImages, }; } @@ -1715,19 +1765,27 @@ function dryRun(selectedAssets, size) { { mode: 'dry-run', assets: selectedAssets.map((asset) => { - const body = buildRequestBody(asset, size); + const { referenceImages = [], ...body } = buildRequestBody(asset, size); return { id: asset.id, + endpoint: referenceImages.length + ? '/v1/images/edits' + : '/v1/images/generations', outputPath: outputPathFor(asset), sourceOutputPath: asset.transparent ? sourceOutputPathFor(asset) : undefined, transparent: asset.transparent, localPostprocess: asset.localPostprocess, - body: { - ...body, - image: body.image ? [''] : undefined, - }, + body: referenceImages.length ? undefined : body, + form: referenceImages.length + ? { + ...body, + imageParts: referenceImages.map( + (referenceImage) => referenceImage.fileName, + ), + } + : undefined, }; }), }, diff --git a/scripts/generate-match3d-style-references.mjs b/scripts/generate-match3d-style-references.mjs index 653b5fd0..fe5ad729 100644 --- a/scripts/generate-match3d-style-references.mjs +++ b/scripts/generate-match3d-style-references.mjs @@ -1,4 +1,4 @@ -import { Buffer } from 'node:buffer'; +import { Buffer } from 'node:buffer'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -215,7 +215,7 @@ async function downloadImage(url, timeoutMs) { async function generateOne(env, template, outDir, size) { const requestBody = { - model: 'gpt-image-2-all', + model: 'gpt-image-2', prompt: buildPrompt(template), n: 1, size, @@ -278,7 +278,7 @@ if (dryRun) { id: template.id, title: template.title, body: { - model: 'gpt-image-2-all', + model: 'gpt-image-2', prompt: buildPrompt(template), n: 1, size, diff --git a/scripts/generate-taonier-logo-concepts.mjs b/scripts/generate-taonier-logo-concepts.mjs index 2faf51b9..a26a9587 100644 --- a/scripts/generate-taonier-logo-concepts.mjs +++ b/scripts/generate-taonier-logo-concepts.mjs @@ -817,12 +817,18 @@ function resolveEnv() { }; } -function buildUrl(baseUrl) { +function buildGenerationUrl(baseUrl) { return baseUrl.endsWith('/v1') ? `${baseUrl}/images/generations` : `${baseUrl}/v1/images/generations`; } +function buildEditUrl(baseUrl) { + return baseUrl.endsWith('/v1') + ? `${baseUrl}/images/edits` + : `${baseUrl}/v1/images/edits`; +} + function hasHeader(headers, targetName) { return Object.keys(headers).some( (name) => name.toLowerCase() === targetName.toLowerCase(), @@ -954,7 +960,7 @@ function inferExtensionFromBytes(bytes) { return 'png'; } -function imagePathToDataUrl(imagePath) { +function imagePathToReferenceImage(imagePath) { if (!existsSync(imagePath)) { throw new Error(`Reference image not found: ${imagePath}`); } @@ -967,7 +973,44 @@ function imagePathToDataUrl(imagePath) { : extension === '.webp' ? 'image/webp' : 'image/png'; - return `data:${mimeType};base64,${bytes.toString('base64')}`; + return { + fieldName: 'image', + fileName: path.basename(imagePath).replace(/"/gu, '_'), + mimeType, + bytes, + }; +} + +function buildMultipartBody(fields, files) { + const boundary = `----genarrative-${Date.now().toString(16)}-${Math.random() + .toString(16) + .slice(2)}`; + const chunks = []; + const push = (value) => { + chunks.push(Buffer.isBuffer(value) ? value : Buffer.from(value)); + }; + + for (const [name, value] of Object.entries(fields)) { + push(`--${boundary}\r\n`); + push(`Content-Disposition: form-data; name="${name}"\r\n\r\n`); + push(`${value}\r\n`); + } + + for (const file of files) { + push(`--${boundary}\r\n`); + push( + `Content-Disposition: form-data; name="${file.fieldName}"; filename="${file.fileName}"\r\n`, + ); + push(`Content-Type: ${file.mimeType}\r\n\r\n`); + push(file.bytes); + push('\r\n'); + } + + push(`--${boundary}--\r\n`); + return { + body: Buffer.concat(chunks), + contentType: `multipart/form-data; boundary=${boundary}`, + }; } async function fetchJson(url, options, timeoutMs) { @@ -1011,27 +1054,45 @@ async function downloadUrl(url, timeoutMs) { async function generateConcept(env, concept) { const requestBody = { - model: 'gpt-image-2-all', + model: 'gpt-image-2', prompt: concept.prompt, n: 1, size: '1024x1024', }; - if (concept.referenceImages?.length) { - requestBody.image = concept.referenceImages.map(imagePathToDataUrl); - } - const payload = await fetchJson( - buildUrl(env.baseUrl), - { - method: 'POST', - headers: { - Authorization: `Bearer ${env.apiKey}`, - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBody), - }, - env.timeoutMs, + + const referenceImages = (concept.referenceImages || []).map( + imagePathToReferenceImage, ); + const payload = referenceImages.length + ? await (async () => { + const multipart = buildMultipartBody(requestBody, referenceImages); + return fetchJson( + buildEditUrl(env.baseUrl), + { + method: 'POST', + headers: { + Authorization: `Bearer ${env.apiKey}`, + Accept: 'application/json', + 'Content-Type': multipart.contentType, + }, + body: multipart.body, + }, + env.timeoutMs, + ); + })() + : await fetchJson( + buildGenerationUrl(env.baseUrl), + { + method: 'POST', + headers: { + Authorization: `Bearer ${env.apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }, + env.timeoutMs, + ); const urls = extractImageUrls(payload); const b64Images = extractBase64Images(payload); @@ -1072,19 +1133,28 @@ if (dryRun) { requests: selected.map((concept) => ({ id: concept.id, title: concept.title, - body: { - model: 'gpt-image-2-all', - prompt: concept.prompt, - n: 1, - size: '1024x1024', - ...(concept.referenceImages?.length - ? { - image: concept.referenceImages.map((imagePath) => - path.relative(repoRoot, imagePath), - ), - } - : {}), - }, + endpoint: concept.referenceImages?.length + ? '/v1/images/edits' + : '/v1/images/generations', + body: concept.referenceImages?.length + ? undefined + : { + model: 'gpt-image-2', + prompt: concept.prompt, + n: 1, + size: '1024x1024', + }, + form: concept.referenceImages?.length + ? { + model: 'gpt-image-2', + prompt: concept.prompt, + n: 1, + size: '1024x1024', + imageParts: concept.referenceImages.map((imagePath) => + path.relative(repoRoot, imagePath), + ), + } + : undefined, })), }, null, diff --git a/scripts/jenkins-server-provision.sh b/scripts/jenkins-server-provision.sh index b584b90b..e54b42d0 100755 --- a/scripts/jenkins-server-provision.sh +++ b/scripts/jenkins-server-provision.sh @@ -292,6 +292,42 @@ write_env_value() { chown root:root "${file}" } +ensure_env_value() { + local file="$1" + local key="$2" + local default_value="$3" + local current_value + + current_value="$(read_env_value "${file}" "${key}")" + if [[ -n "${current_value}" ]]; then + return + fi + + echo "[server-provision] 补齐 api-server 环境变量: ${key} -> ${file}" + if [[ "${DRY_RUN}" != "true" ]]; then + write_env_value "${file}" "${key}" "${default_value}" + fi +} + +ensure_api_runtime_env_defaults() { + if [[ "${DRY_RUN}" == "true" ]]; then + echo "+ ensure api-server runtime env defaults in ${API_ENV_FILE}" + return + fi + if [[ ! -f "${API_ENV_FILE}" ]]; then + echo "[server-provision] 环境文件不存在,无法补齐 api-server 运行态目录变量: ${API_ENV_FILE}" >&2 + exit 1 + fi + + # 已存在的生产 env 会被保留,不会整文件覆盖;这里仅补后续版本新增的运行态写入路径。 + ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_TRACKING_OUTBOX_ENABLED" "true" + ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_TRACKING_OUTBOX_DIR" "/var/lib/genarrative/tracking-outbox" + ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_TRACKING_OUTBOX_BATCH_SIZE" "500" + ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_TRACKING_OUTBOX_FLUSH_INTERVAL_MS" "1000" + ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_TRACKING_OUTBOX_MAX_BYTES" "268435456" + ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_AUTH_STORE_PATH" "/var/lib/genarrative/auth/auth-store.json" +} + parse_json_string_field() { local json="$1" local key="$2" @@ -437,11 +473,55 @@ validate_nginx_tls() { fi } +disable_nginx_default_sites_enabled() { + local moves_file="$1" + local sites_enabled="/etc/nginx/sites-enabled" + local sites_disabled="/etc/nginx/sites-disabled" + local stamp source target base + local candidates=("${sites_enabled}/default" "${sites_enabled}/default."*) + + stamp="$(date +%Y%m%d%H%M%S)" + for source in "${candidates[@]}"; do + if [[ ! -e "${source}" && ! -L "${source}" ]]; then + continue + fi + + base="$(basename "${source}")" + target="${sites_disabled}/${base}.disabled-${stamp}" + echo "[server-provision] 禁用 Debian 默认 Nginx 站点,避免与 Genarrative server_name 冲突: ${source} -> ${target}" + mkdir -p "${sites_disabled}" + mv "${source}" "${target}" + printf "%s\t%s\n" "${target}" "${source}" >>"${moves_file}" + done +} + +restore_nginx_default_sites_enabled() { + local moves_file="$1" + local target source + + if [[ ! -f "${moves_file}" ]]; then + return + fi + + while IFS=$'\t' read -r target source || [[ -n "${target:-}" ]]; do + if [[ -z "${target:-}" || -z "${source:-}" ]]; then + continue + fi + if [[ -e "${target}" || -L "${target}" ]]; then + mkdir -p "$(dirname "${source}")" + if [[ ! -e "${source}" && ! -L "${source}" ]]; then + echo "[server-provision] 恢复 Debian 默认 Nginx 站点: ${target} -> ${source}" + mv "${target}" "${source}" + fi + fi + done <"${moves_file}" +} + install_nginx_config_with_rollback() { local config_target="/etc/nginx/conf.d/genarrative.conf" local snippet_target="/etc/nginx/snippets/genarrative-maintenance.conf" local config_source - local rendered_config rendered_snippet config_backup snippet_backup + local rendered_config rendered_snippet config_backup snippet_backup disabled_sites local had_config="false" local had_snippet="false" @@ -459,6 +539,7 @@ install_nginx_config_with_rollback() { echo "+ install -m 0644 deploy/nginx/snippets/genarrative-maintenance.conf ${snippet_target}" if [[ "${DRY_RUN}" == "true" ]]; then + echo "+ disable /etc/nginx/sites-enabled/default* if present" echo "+ nginx -t" echo "+ nginx -s reload" return @@ -468,6 +549,7 @@ install_nginx_config_with_rollback() { rendered_snippet="$(mktemp)" config_backup="$(mktemp)" snippet_backup="$(mktemp)" + disabled_sites="$(mktemp)" if [[ "${NGINX_CONFIG_MODE}" == "production-https" ]]; then validate_nginx_tls render_nginx_https_config >"${rendered_config}" @@ -487,6 +569,7 @@ install_nginx_config_with_rollback() { install -m 0644 "${rendered_config}" "${config_target}" install -m 0644 "${rendered_snippet}" "${snippet_target}" + disable_nginx_default_sites_enabled "${disabled_sites}" if ! nginx -t; then echo "[server-provision] nginx -t 失败,恢复写入前的 Nginx 配置。" >&2 @@ -500,13 +583,14 @@ install_nginx_config_with_rollback() { else rm -f "${snippet_target}" fi - rm -f "${rendered_config}" "${rendered_snippet}" "${config_backup}" "${snippet_backup}" + restore_nginx_default_sites_enabled "${disabled_sites}" + rm -f "${rendered_config}" "${rendered_snippet}" "${config_backup}" "${snippet_backup}" "${disabled_sites}" exit 1 fi echo "+ nginx -s reload" nginx -s reload - rm -f "${rendered_config}" "${rendered_snippet}" "${config_backup}" "${snippet_backup}" + rm -f "${rendered_config}" "${rendered_snippet}" "${config_backup}" "${snippet_backup}" "${disabled_sites}" } cleanup_placeholder_nginx_config() { @@ -625,6 +709,7 @@ if [[ ! -f "${API_ENV_FILE}" ]]; then else echo "[server-provision] 已存在环境文件,保留不覆盖: ${API_ENV_FILE}" fi +ensure_api_runtime_env_defaults if [[ "${ENABLE_OTELCOL:-true}" == "true" ]]; then sync_otelcol_install diff --git a/scripts/prepare-server-provision-tools.sh b/scripts/prepare-server-provision-tools.sh index 2e73a5f4..f9ff18e8 100755 --- a/scripts/prepare-server-provision-tools.sh +++ b/scripts/prepare-server-provision-tools.sh @@ -2,15 +2,24 @@ set -euo pipefail PROVISION_TOOLS_DIR="${PROVISION_TOOLS_DIR:-provision-tools}" +PROVISION_DOWNLOADS_DIR="${PROVISION_DOWNLOADS_DIR:-}" OTELCOL_VERSION="${OTELCOL_VERSION:-0.151.0}" PREPARE_OTELCOL="${PREPARE_OTELCOL:-${ENABLE_OTELCOL:-true}}" -OTELCOL_ARCHIVE_SOURCE="${OTELCOL_ARCHIVE_SOURCE:-}" OTELCOL_DOWNLOAD_ROOT="${OTELCOL_DOWNLOAD_ROOT:-https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download}" +OTELCOL_ARCHIVE_PATH="${OTELCOL_ARCHIVE_PATH:-}" SPACETIME_INSTALLER_URL="${SPACETIME_INSTALLER_URL:-https://install.spacetimedb.com}" SPACETIME_DOWNLOAD_ROOT="${SPACETIME_DOWNLOAD_ROOT:-https://github.com/clockworklabs/SpacetimeDB/releases/latest/download}" +SPACETIME_TARGET_HOST="${SPACETIME_TARGET_HOST:-x86_64-unknown-linux-gnu}" +SPACETIME_ARCHIVE_PATH="${SPACETIME_ARCHIVE_PATH:-}" +SPACETIME_INSTALLER_PATH="${SPACETIME_INSTALLER_PATH:-}" +SPACETIME_UPDATE_INSTALLER_PATH="${SPACETIME_UPDATE_INSTALLER_PATH:-}" +PROVISION_DOWNLOAD_PROXY="${PROVISION_DOWNLOAD_PROXY:-}" +PROVISION_NO_PROXY="${PROVISION_NO_PROXY:-127.0.0.1,localhost}" +PROVISION_REQUIRE_LOCAL_DOWNLOADS="${PROVISION_REQUIRE_LOCAL_DOWNLOADS:-false}" PROVISION_TOOLS_TMP_PARENT="${PROVISION_TOOLS_TMP_PARENT:-${WORKSPACE:-$(pwd)}/.tmp/server-provision-tools}" TMP_DIR_TO_CLEAN="" OTELCOL_SOURCE_DESCRIPTION="skipped" +SPACETIME_SOURCE_DESCRIPTION="unset" cleanup_tmp_dir() { if [[ -n "${TMP_DIR_TO_CLEAN}" ]]; then @@ -26,6 +35,22 @@ require_cmd() { fi } +configure_download_proxy() { + if [[ -z "${PROVISION_DOWNLOAD_PROXY}" ]]; then + return + fi + + export HTTP_PROXY="${PROVISION_DOWNLOAD_PROXY}" + export HTTPS_PROXY="${PROVISION_DOWNLOAD_PROXY}" + export ALL_PROXY="${PROVISION_DOWNLOAD_PROXY}" + export http_proxy="${PROVISION_DOWNLOAD_PROXY}" + export https_proxy="${PROVISION_DOWNLOAD_PROXY}" + export all_proxy="${PROVISION_DOWNLOAD_PROXY}" + export NO_PROXY="${PROVISION_NO_PROXY}" + export no_proxy="${PROVISION_NO_PROXY}" + echo "[prepare-provision-tools] 已配置下载代理: ${PROVISION_DOWNLOAD_PROXY%%://*}://***" +} + download_file() { local url="$1" local output="$2" @@ -40,6 +65,19 @@ download_file() { fi } +validate_relative_dir() { + local label="$1" + local path="$2" + + if [[ -z "${path}" ]]; then + return + fi + if [[ "${path}" == /* || "${path}" == *..* || "${path}" == "." ]]; then + echo "[prepare-provision-tools] ${label} 只能是工作区内的相对路径: ${path}" >&2 + exit 1 + fi +} + make_spacetime_wrapper() { local target="$1" @@ -57,20 +95,27 @@ prepare_otelcol() { local archive="${tmp_dir}/otelcol-contrib.tar.gz" local extract_dir="${tmp_dir}/otelcol-contrib" local url="${OTELCOL_DOWNLOAD_ROOT}/v${OTELCOL_VERSION}/otelcol-contrib_${OTELCOL_VERSION}_linux_amd64.tar.gz" - local source_archive="${OTELCOL_ARCHIVE_SOURCE}" + local downloaded_archive="${PROVISION_DOWNLOADS_DIR}/otelcol-contrib_${OTELCOL_VERSION}_linux_amd64.tar.gz" + local source_archive="" local target="${PROVISION_TOOLS_DIR}/otelcol-contrib" require_cmd tar + if [[ -n "${OTELCOL_ARCHIVE_PATH}" && -f "${OTELCOL_ARCHIVE_PATH}" ]]; then + source_archive="${OTELCOL_ARCHIVE_PATH}" + elif [[ -n "${PROVISION_DOWNLOADS_DIR}" && -f "${downloaded_archive}" ]]; then + source_archive="${downloaded_archive}" + fi + if [[ "${PROVISION_REQUIRE_LOCAL_DOWNLOADS}" == "true" && -z "${source_archive}" ]]; then + echo "[prepare-provision-tools] 要求使用 Windows 已下载的 otelcol-contrib 包,但未找到: ${downloaded_archive}" >&2 + exit 1 + fi + mkdir -p "${extract_dir}" if [[ -n "${source_archive}" ]]; then - if [[ ! -f "${source_archive}" ]]; then - echo "[prepare-provision-tools] 上传的 otelcol-contrib 包不存在: ${source_archive}" >&2 - exit 1 - fi - echo "[prepare-provision-tools] 使用手动上传的 otelcol-contrib 包: ${source_archive}" + echo "[prepare-provision-tools] 使用已下载的 otelcol-contrib 包: ${source_archive}" cp "${source_archive}" "${archive}" - OTELCOL_SOURCE_DESCRIPTION="manual archive ${source_archive}" + OTELCOL_SOURCE_DESCRIPTION="local ${source_archive}" else echo "[prepare-provision-tools] 下载 otelcol-contrib: ${url}" download_file "${url}" "${archive}" @@ -91,12 +136,68 @@ prepare_spacetime() { local tmp_dir="$1" local install_root="${tmp_dir}/spacetime-root" local target_dir="${PROVISION_TOOLS_DIR}/spacetime" + local archive_name="spacetime-${SPACETIME_TARGET_HOST}.tar.gz" + local downloaded_archive="${PROVISION_DOWNLOADS_DIR}/${archive_name}" + local source_archive="" + local update_name="spacetimedb-update-${SPACETIME_TARGET_HOST}" + local downloaded_update="${PROVISION_DOWNLOADS_DIR}/${update_name}" + local source_update="" + local prepared_update="${tmp_dir}/spacetimedb-update" + local downloaded_installer="${PROVISION_DOWNLOADS_DIR}/spacetime-install.sh" + local source_installer="" - echo "[prepare-provision-tools] 使用官方安装器准备 SpacetimeDB: ${SPACETIME_INSTALLER_URL}" mkdir -p "${install_root}" - download_file "${SPACETIME_INSTALLER_URL}" "${tmp_dir}/spacetime-install.sh" - chmod 0755 "${tmp_dir}/spacetime-install.sh" - TMPDIR="${tmp_dir}" SPACETIME_DOWNLOAD_ROOT="${SPACETIME_DOWNLOAD_ROOT}" sh "${tmp_dir}/spacetime-install.sh" --root-dir "${install_root}" -y + if [[ -n "${SPACETIME_ARCHIVE_PATH}" && -f "${SPACETIME_ARCHIVE_PATH}" ]]; then + source_archive="${SPACETIME_ARCHIVE_PATH}" + elif [[ -n "${PROVISION_DOWNLOADS_DIR}" && -f "${downloaded_archive}" ]]; then + source_archive="${downloaded_archive}" + fi + + if [[ -n "${source_archive}" ]]; then + echo "[prepare-provision-tools] 使用已下载的 SpacetimeDB release tarball: ${source_archive}" + mkdir -p "${install_root}/bin/current" + tar -xzf "${source_archive}" -C "${install_root}/bin/current" + chmod 0755 "${install_root}/bin/current/spacetimedb-cli" "${install_root}/bin/current/spacetimedb-standalone" + SPACETIME_SOURCE_DESCRIPTION="local archive ${source_archive}" + elif [[ -n "${SPACETIME_UPDATE_INSTALLER_PATH}" && -f "${SPACETIME_UPDATE_INSTALLER_PATH}" ]]; then + source_update="${SPACETIME_UPDATE_INSTALLER_PATH}" + elif [[ -n "${PROVISION_DOWNLOADS_DIR}" && -f "${downloaded_update}" ]]; then + source_update="${downloaded_update}" + fi + if [[ "${PROVISION_REQUIRE_LOCAL_DOWNLOADS}" == "true" && -z "${source_archive}" ]]; then + echo "[prepare-provision-tools] 要求使用 Windows 已下载的 SpacetimeDB release tarball,但未找到: ${downloaded_archive}" >&2 + exit 1 + fi + + if [[ -n "${source_archive}" ]]; then + : + elif [[ -n "${source_update}" ]]; then + echo "[prepare-provision-tools] 使用已下载的 SpacetimeDB Linux update installer: ${source_update}" + cp "${source_update}" "${prepared_update}" + chmod 0755 "${prepared_update}" + TMPDIR="${tmp_dir}" "${prepared_update}" --root-dir "${install_root}" -y + SPACETIME_SOURCE_DESCRIPTION="local update ${source_update}" + else + if [[ -n "${SPACETIME_INSTALLER_PATH}" && -f "${SPACETIME_INSTALLER_PATH}" ]]; then + source_installer="${SPACETIME_INSTALLER_PATH}" + elif [[ -n "${PROVISION_DOWNLOADS_DIR}" && -f "${downloaded_installer}" ]]; then + source_installer="${downloaded_installer}" + fi + + if [[ "${PROVISION_REQUIRE_LOCAL_DOWNLOADS}" == "true" && -z "${source_installer}" ]]; then + echo "[prepare-provision-tools] 要求使用 Windows 已下载的 SpacetimeDB 官方安装器脚本,但未找到: ${downloaded_installer}" >&2 + exit 1 + elif [[ -n "${source_installer}" ]]; then + echo "[prepare-provision-tools] 使用已下载的 SpacetimeDB 官方安装器脚本: ${source_installer}" + cp "${source_installer}" "${tmp_dir}/spacetime-install.sh" + else + echo "[prepare-provision-tools] 下载 SpacetimeDB 官方安装器脚本: ${SPACETIME_INSTALLER_URL}" + download_file "${SPACETIME_INSTALLER_URL}" "${tmp_dir}/spacetime-install.sh" + fi + chmod 0755 "${tmp_dir}/spacetime-install.sh" + TMPDIR="${tmp_dir}" SPACETIME_DOWNLOAD_ROOT="${SPACETIME_DOWNLOAD_ROOT}" sh "${tmp_dir}/spacetime-install.sh" --root-dir "${install_root}" -y + SPACETIME_SOURCE_DESCRIPTION="installer ${SPACETIME_INSTALLER_URL}; download root ${SPACETIME_DOWNLOAD_ROOT}" + fi if [[ ! -x "${install_root}/bin/current/spacetimedb-cli" ]]; then echo "[prepare-provision-tools] SpacetimeDB 安装结果缺少 bin/current/spacetimedb-cli。" >&2 @@ -122,6 +223,16 @@ main() { require_cmd install require_cmd mktemp require_cmd rm + require_cmd tar + + validate_relative_dir "PROVISION_TOOLS_DIR" "${PROVISION_TOOLS_DIR}" + validate_relative_dir "PROVISION_DOWNLOADS_DIR" "${PROVISION_DOWNLOADS_DIR}" + if [[ -n "${PROVISION_DOWNLOADS_DIR}" && "${PROVISION_DOWNLOADS_DIR%/}" == "${PROVISION_TOOLS_DIR%/}" ]]; then + echo "[prepare-provision-tools] PROVISION_DOWNLOADS_DIR 不能等于 PROVISION_TOOLS_DIR,否则会被清理: ${PROVISION_DOWNLOADS_DIR}" >&2 + exit 1 + fi + + configure_download_proxy mkdir -p "${PROVISION_TOOLS_TMP_PARENT}" tmp_dir="$(mktemp -d "${PROVISION_TOOLS_TMP_PARENT%/}/run.XXXXXX")" @@ -140,8 +251,9 @@ main() { cat >"${PROVISION_TOOLS_DIR}/MANIFEST.txt" < Router { .merge(modules::bark_battle::router(state.clone())) .merge(modules::match3d::router(state.clone())) .merge(modules::square_hole::router(state.clone())) + .merge(modules::jump_hop::router(state.clone())) .merge(modules::puzzle::router(state.clone())) .merge(visual_novel_router(state.clone())) .route( @@ -618,6 +619,31 @@ mod tests { assert_eq!(body["error"]["details"]["creationTypeId"], "visual-novel"); } + #[tokio::test] + async fn disabled_rpg_route_returns_service_unavailable() { + let state = AppState::new(AppConfig::default()).expect("state should build"); + state.set_test_creation_entry_route_enabled("rpg", false); + let app = build_router(state); + + let response = app + .oneshot( + Request::builder() + .uri("/api/runtime/custom-world/agent/sessions") + .body(Body::empty()) + .expect("request should build"), + ) + .await + .expect("request should succeed"); + + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + let body = read_json_response(response).await; + assert_eq!( + body["error"]["details"]["reason"], + "creation_entry_disabled" + ); + assert_eq!(body["error"]["details"]["creationTypeId"], "rpg"); + } + #[tokio::test] async fn healthz_returns_standard_envelope_when_requested() { let app = build_router(AppState::new(AppConfig::default()).expect("state should build")); diff --git a/server-rs/crates/api-server/src/assets.rs b/server-rs/crates/api-server/src/assets.rs index 33d46ae5..1c709b96 100644 --- a/server-rs/crates/api-server/src/assets.rs +++ b/server-rs/crates/api-server/src/assets.rs @@ -394,9 +394,13 @@ pub async fn confirm_asset_object( let result = state .spacetime_client() .confirm_asset_object( - build_confirm_asset_object_upsert_input(oss_client, payload) - .await - .map_err(map_confirm_asset_object_prepare_error)?, + build_confirm_asset_object_upsert_input( + oss_client, + payload, + authenticated.claims().user_id(), + ) + .await + .map_err(map_confirm_asset_object_prepare_error)?, ) .await .map_err(map_confirm_asset_object_error)?; @@ -592,6 +596,7 @@ fn supported_asset_history_kind_message() -> String { async fn build_confirm_asset_object_upsert_input( oss_client: &platform_oss::OssClient, payload: ConfirmAssetObjectRequest, + authenticated_owner_user_id: &str, ) -> Result { let configured_bucket = oss_client.config_bucket().to_string(); let resolved_bucket = payload @@ -629,6 +634,14 @@ async fn build_confirm_asset_object_upsert_input( { return Err(ConfirmAssetObjectPrepareError::ContentLengthMismatch); } + let owner_user_id = normalize_optional_value(payload.owner_user_id).or_else(|| { + let owner = authenticated_owner_user_id.trim(); + if owner.is_empty() { + None + } else { + Some(owner.to_string()) + } + }); let now_micros = current_utc_micros(); build_asset_object_upsert_input( @@ -645,7 +658,7 @@ async fn build_confirm_asset_object_upsert_input( normalize_optional_value(payload.content_hash), payload.asset_kind, payload.source_job_id, - payload.owner_user_id, + owner_user_id, payload.profile_id, payload.entity_id, now_micros, diff --git a/server-rs/crates/api-server/src/config.rs b/server-rs/crates/api-server/src/config.rs index 0398c948..079f7b5a 100644 --- a/server-rs/crates/api-server/src/config.rs +++ b/server-rs/crates/api-server/src/config.rs @@ -260,8 +260,11 @@ impl Default for AppConfig { llm_request_timeout_ms: DEFAULT_REQUEST_TIMEOUT_MS, llm_max_retries: DEFAULT_MAX_RETRIES, llm_retry_backoff_ms: DEFAULT_RETRY_BACKOFF_MS, - rpg_llm_web_search_enabled: true, - creation_agent_llm_web_search_enabled: true, + // 中文注释:创作/RPG 的结构化 JSON 链路默认不启用 Responses web_search。 + // 未开通工具的上游会先吐自然语言再返回 ToolNotOpen,容易污染严格 JSON 结果; + // 需要联网增强时由部署环境显式打开对应开关。 + rpg_llm_web_search_enabled: false, + creation_agent_llm_web_search_enabled: false, dashscope_base_url: "https://dashscope.aliyuncs.com/api/v1".to_string(), dashscope_api_key: None, dashscope_scene_image_model: String::new(), @@ -1467,6 +1470,14 @@ mod tests { } } + #[test] + fn default_keeps_structured_llm_web_search_disabled() { + let config = AppConfig::default(); + + assert!(!config.rpg_llm_web_search_enabled); + assert!(!config.creation_agent_llm_web_search_enabled); + } + #[test] fn from_env_reads_rpg_llm_web_search_switch() { let _guard = ENV_LOCK @@ -1476,11 +1487,11 @@ mod tests { unsafe { std::env::remove_var("GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED"); - std::env::set_var("GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED", "false"); + std::env::set_var("GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED", "true"); } let config = AppConfig::from_env(); - assert!(!config.rpg_llm_web_search_enabled); + assert!(config.rpg_llm_web_search_enabled); unsafe { std::env::remove_var("GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED"); @@ -1496,11 +1507,11 @@ mod tests { unsafe { std::env::remove_var("GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED"); - std::env::set_var("GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED", "false"); + std::env::set_var("GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED", "true"); } let config = AppConfig::from_env(); - assert!(!config.creation_agent_llm_web_search_enabled); + assert!(config.creation_agent_llm_web_search_enabled); unsafe { std::env::remove_var("GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED"); diff --git a/server-rs/crates/api-server/src/creation_agent_llm_turn.rs b/server-rs/crates/api-server/src/creation_agent_llm_turn.rs index 3a2cf825..86bbfa93 100644 --- a/server-rs/crates/api-server/src/creation_agent_llm_turn.rs +++ b/server-rs/crates/api-server/src/creation_agent_llm_turn.rs @@ -93,15 +93,74 @@ where F: FnMut(&str), { let mut latest_reply_text = String::new(); + let turn_output = match request_stream_creation_agent_json_turn_once( + llm_client, + system_prompt.clone(), + user_prompt.clone(), + enable_web_search, + on_reply_update, + &mut latest_reply_text, + !enable_web_search, + ) + .await + { + Ok(turn_output) => turn_output, + Err(CreationAgentJsonTurnFailure::Stream(error)) + if enable_web_search && is_web_search_tool_unavailable(&error) => + { + tracing::warn!( + error = %error, + "创作 Agent 流式联网搜索插件不可用,自动降级为无联网搜索重试" + ); + latest_reply_text.clear(); + request_stream_creation_agent_json_turn_once( + llm_client, + system_prompt, + user_prompt, + false, + on_reply_update, + &mut latest_reply_text, + true, + ) + .await? + } + Err(error) => return Err(error), + }; + + let reply_text = read_reply_text(&turn_output.parsed); + if let Some(reply_text) = reply_text.as_deref() + && reply_text != latest_reply_text + { + on_reply_update(reply_text); + } + + Ok(turn_output) +} + +async fn request_stream_creation_agent_json_turn_once( + llm_client: &LlmClient, + system_prompt: String, + user_prompt: String, + enable_web_search: bool, + on_reply_update: &mut F, + latest_reply_text: &mut String, + emit_reply_updates: bool, +) -> Result +where + F: FnMut(&str), +{ let response = llm_client .stream_text( build_creation_agent_llm_request(system_prompt, user_prompt, enable_web_search), |delta: &LlmStreamDelta| { + if !emit_reply_updates { + return; + } if let Some(reply_progress) = extract_reply_text_from_partial_json(delta.accumulated_text.as_str()) - && reply_progress != latest_reply_text + && reply_progress != *latest_reply_text { - latest_reply_text = reply_progress.clone(); + *latest_reply_text = reply_progress.clone(); on_reply_update(reply_progress.as_str()); } }, @@ -110,12 +169,6 @@ where .map_err(CreationAgentJsonTurnFailure::Stream)?; let parsed = parse_json_response_text(response.content.as_str()) .map_err(|_| CreationAgentJsonTurnFailure::Parse)?; - let reply_text = read_reply_text(&parsed); - if let Some(reply_text) = reply_text.as_deref() - && reply_text != latest_reply_text - { - on_reply_update(reply_text); - } Ok(CreationAgentJsonTurnOutput { parsed }) } @@ -327,6 +380,7 @@ mod tests { let server = spawn_capturing_mock_server(vec![ MockResponse { body: concat!( + "data: {\"type\":\"response.output_text.delta\",\"delta\":\"我需要先搜索玩具王国资料。\"}\n\n", "data: {\"type\":\"error\",\"code\":\"ToolNotOpen\",\"message\":\"Your account has not activated web search.\"}\n\n", "data: [DONE]\n\n" ) @@ -391,6 +445,55 @@ mod tests { } } + #[tokio::test] + async fn stream_turn_keeps_partial_updates_when_web_search_is_disabled() { + let server = spawn_capturing_mock_server(vec![MockResponse { + body: concat!( + "data: {\"type\":\"response.output_text.delta\",\"delta\":\"{\\\"replyText\\\":\\\"我先\"}\n\n", + "data: {\"type\":\"response.output_text.delta\",\"delta\":\"把玩具王国定住。\\\",\\\"progressPercent\\\":12}\"}\n\n", + "data: {\"type\":\"response.completed\"}\n\n", + ) + .to_string(), + }]); + let config = LlmConfig::new( + LlmProvider::Ark, + server.base_url, + "test-key".to_string(), + "test-model".to_string(), + 30_000, + 0, + 1, + ) + .expect("LLM config should build"); + let llm_client = platform_llm::LlmClient::new(config).expect("LLM client should build"); + let mut visible_replies = Vec::new(); + + let output = stream_creation_agent_json_turn( + Some(&llm_client), + "系统提示".to_string(), + "用户提示", + false, + CreationAgentLlmTurnErrorMessages { + model_unavailable: "模型不可用", + generation_failed: "生成失败", + parse_failed: "解析失败", + }, + |text| visible_replies.push(text.to_string()), + |message| message, + ) + .await + .expect("stream without web search should succeed"); + + assert_eq!( + output.parsed["replyText"].as_str(), + Some("我先把玩具王国定住。") + ); + assert_eq!( + visible_replies, + vec!["我先".to_string(), "我先把玩具王国定住。".to_string()] + ); + } + struct MockResponse { body: String, } 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 bb1d547e..fe6ea73c 100644 --- a/server-rs/crates/api-server/src/creation_entry_config.rs +++ b/server-rs/crates/api-server/src/creation_entry_config.rs @@ -87,9 +87,23 @@ pub fn resolve_creation_entry_route_id(path: &str) -> Option<&'static str> { if normalized.starts_with("/api/runtime/square-hole") { return Some("square-hole"); } + if normalized.starts_with("/api/runtime/jump-hop") { + return Some("jump-hop"); + } + if normalized.starts_with("/api/creation/jump-hop") { + return Some("jump-hop"); + } if normalized.starts_with("/api/runtime/big-fish") { return Some("big-fish"); } + if normalized.starts_with("/api/runtime/custom-world") + || normalized.starts_with("/api/runtime/custom-world-library") + || normalized.starts_with("/api/runtime/custom-world-gallery") + || normalized.starts_with("/api/runtime/chat") + || normalized.starts_with("/api/story") + { + return Some("rpg"); + } if normalized.starts_with("/api/runtime/visual-novel") { return Some("visual-novel"); } @@ -155,6 +169,26 @@ mod tests { resolve_creation_entry_route_id("/api/creation/visual-novel/sessions"), Some("visual-novel"), ); + assert_eq!( + resolve_creation_entry_route_id("/api/runtime/custom-world/agent/sessions"), + Some("rpg"), + ); + assert_eq!( + resolve_creation_entry_route_id("/api/runtime/custom-world-library/profile-1"), + Some("rpg"), + ); + assert_eq!( + resolve_creation_entry_route_id("/api/runtime/custom-world-gallery/user-1/profile-1"), + Some("rpg"), + ); + assert_eq!( + resolve_creation_entry_route_id("/api/story/sessions/runtime"), + Some("rpg"), + ); + assert_eq!( + resolve_creation_entry_route_id("/api/runtime/chat/npc/turn/stream"), + Some("rpg"), + ); assert_eq!( resolve_creation_entry_route_id("/api/runtime/bark-battle/works/work-1/config"), Some("bark-battle"), diff --git a/server-rs/crates/api-server/src/custom_world.rs b/server-rs/crates/api-server/src/custom_world.rs index d5fcb403..8695904b 100644 --- a/server-rs/crates/api-server/src/custom_world.rs +++ b/server-rs/crates/api-server/src/custom_world.rs @@ -1626,39 +1626,20 @@ pub async fn execute_custom_world_agent_action( ) })? } else if action == "publish_world" { - let mut publish_payload = serde_json::to_value(&payload).map_err(|error| { + let publish_payload = serialize_publish_world_action_payload( + resolve_author_public_user_code(&state, &authenticated, &request_context)?, + resolve_author_display_name(&state, &authenticated), + ) + .map_err(|error| { custom_world_error_response( &request_context, AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "custom-world-agent", - "message": format!("action payload JSON 序列化失败:{error}"), + "message": error, })), ) })?; - if let Some(object) = publish_payload.as_object_mut() { - // 发布到广场时必须写入真实作者公开信息,避免 gallery 投影落成匿名兜底数据。 - object.insert( - "authorPublicUserCode".to_string(), - Value::String(resolve_author_public_user_code( - &state, - &authenticated, - &request_context, - )?), - ); - object.insert( - "authorDisplayName".to_string(), - Value::String(resolve_author_display_name(&state, &authenticated)), - ); - } - serde_json::to_string(&publish_payload).map_err(|error| { - custom_world_error_response( - &request_context, - AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ - "provider": "custom-world-agent", - "message": format!("action payload JSON 序列化失败:{error}"), - })), - ) - })? + publish_payload } else { serde_json::to_string(&payload).map_err(|error| { custom_world_error_response( @@ -1734,6 +1715,23 @@ fn serialize_sync_result_profile_action_payload( .map_err(|error| format!("action payload JSON 序列化失败:{error}")) } +fn serialize_publish_world_action_payload( + author_public_user_code: String, + author_display_name: String, +) -> Result { + // 中文注释:发布动作只提交动作名和作者公开信息。 + // 结果页当前 profile 必须先通过 sync_result_profile 写入 session; + // SpacetimeDB 发布时再从 session.draft_profile_json 读取草稿真相,避免前端 + // draftProfile / legacyResultProfile / profile 旧载荷覆盖刚保存的内容。 + let payload_value = json!({ + "action": "publish_world", + "authorPublicUserCode": author_public_user_code, + "authorDisplayName": author_display_name, + }); + serde_json::to_string(&payload_value) + .map_err(|error| format!("action payload JSON 序列化失败:{error}")) +} + fn canonicalize_custom_world_library_profile_payload( mut profile: Value, ) -> Result<(Value, CustomWorldProfileMetadata), String> { @@ -3414,6 +3412,36 @@ mod tests { ); } + #[test] + fn publish_world_payload_only_contains_action_and_author_identity() { + let payload_json = + serialize_publish_world_action_payload("TN-0001".to_string(), "潮汐作者".to_string()) + .expect("publish payload serializes"); + let payload_value: Value = + serde_json::from_str(&payload_json).expect("payload should be valid JSON"); + let object = payload_value + .as_object() + .expect("publish payload should be object"); + + assert_eq!(object.len(), 3); + assert_eq!( + object.get("action").and_then(Value::as_str), + Some("publish_world") + ); + assert_eq!( + object.get("authorPublicUserCode").and_then(Value::as_str), + Some("TN-0001") + ); + assert_eq!( + object.get("authorDisplayName").and_then(Value::as_str), + Some("潮汐作者") + ); + assert!(!object.contains_key("profile")); + assert!(!object.contains_key("draftProfile")); + assert!(!object.contains_key("legacyResultProfile")); + assert!(!object.contains_key("settingText")); + } + #[test] fn custom_world_library_profile_payload_is_canonicalized_on_server() { let (profile, metadata) = canonicalize_custom_world_library_profile_payload(json!({ diff --git a/server-rs/crates/api-server/src/custom_world_ai.rs b/server-rs/crates/api-server/src/custom_world_ai.rs index 12280439..649999dd 100644 --- a/server-rs/crates/api-server/src/custom_world_ai.rs +++ b/server-rs/crates/api-server/src/custom_world_ai.rs @@ -10,7 +10,9 @@ use axum::{ response::Response, }; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; -use image::{DynamicImage, GenericImageView, imageops::FilterType}; +use image::{ + DynamicImage, GenericImageView, ImageFormat, codecs::jpeg::JpegEncoder, imageops::FilterType, +}; use module_assets::{ AssetObjectAccessPolicy, AssetObjectFieldError, build_asset_entity_binding_input, build_asset_object_upsert_input, generate_asset_binding_id, generate_asset_object_id, @@ -375,6 +377,8 @@ const OPENING_CG_ENTITY_KIND: &str = "custom_world_profile"; const OPENING_CG_STORYBOARD_SLOT: &str = "opening_cg_storyboard"; const OPENING_CG_VIDEO_SLOT: &str = "opening_cg_video"; const ARK_VIDEO_TASK_POLL_INTERVAL_MS: u64 = 5_000; +const OPENING_CG_REFERENCE_MAX_EDGE: u32 = 768; +const OPENING_CG_REFERENCE_JPEG_QUALITY: u8 = 82; struct CoverPromptContext { opening_act_title: String, @@ -1025,6 +1029,16 @@ pub async fn generate_custom_world_opening_cg( "openingSceneImageSrc", ) .await?; + let player_role_reference = resize_image_reference_data_url( + player_role_reference, + OPENING_CG_REFERENCE_MAX_EDGE, + OPENING_CG_REFERENCE_JPEG_QUALITY, + )?; + let opening_scene_reference = resize_image_reference_data_url( + opening_scene_reference, + OPENING_CG_REFERENCE_MAX_EDGE, + OPENING_CG_REFERENCE_JPEG_QUALITY, + )?; let storyboard = generate_opening_cg_storyboard( &state, &owner_user_id, @@ -1617,6 +1631,52 @@ async fn resolve_reference_image_as_data_url( )) } +fn resize_image_reference_data_url( + data_url: String, + max_edge: u32, + jpeg_quality: u8, +) -> Result { + if max_edge == 0 { + return Ok(data_url); + } + let Some(parsed) = parse_image_data_url(data_url.as_str()) else { + return Ok(data_url); + }; + let image = image::load_from_memory(parsed.bytes.as_slice()).map_err(|error| { + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "custom-world-ai", + "message": format!("无法解析参考图:{error}"), + })) + })?; + let (width, height) = image.dimensions(); + let already_within_budget = width <= max_edge && height <= max_edge; + if already_within_budget && parsed.mime_type == "image/jpeg" { + return Ok(data_url); + } + + // 中文注释:开局 CG 故事板会同时带角色和场景两张参考图;先压到较小 JPEG,避免大图 PNG Data URL 让 VectorEngine 网关在请求发送阶段中断。 + let resized = if already_within_budget { + image + } else { + image.resize(max_edge, max_edge, FilterType::Triangle) + }; + let encoded_image = DynamicImage::ImageRgb8(resized.to_rgb8()); + let mut encoded = Vec::new(); + JpegEncoder::new_with_quality(&mut encoded, jpeg_quality) + .encode_image(&encoded_image) + .map_err(|error| { + AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({ + "provider": "custom-world-ai", + "message": format!("压缩参考图失败:{error}"), + })) + })?; + + Ok(format!( + "data:image/jpeg;base64,{}", + BASE64_STANDARD.encode(encoded) + )) +} + async fn create_text_to_image_generation( http_client: &reqwest::Client, settings: &DashScopeSettings, @@ -3065,6 +3125,34 @@ mod tests { assert_eq!(parsed.bytes, b"hello".to_vec()); } + #[test] + fn opening_cg_reference_data_url_is_resized_to_request_budget() { + let image = DynamicImage::ImageRgb8(image::RgbImage::new(2048, 1152)); + let mut cursor = std::io::Cursor::new(Vec::new()); + image + .write_to(&mut cursor, ImageFormat::Png) + .expect("test image should encode"); + let data_url = format!( + "data:image/png;base64,{}", + BASE64_STANDARD.encode(cursor.into_inner()) + ); + + let resized = resize_image_reference_data_url( + data_url, + OPENING_CG_REFERENCE_MAX_EDGE, + OPENING_CG_REFERENCE_JPEG_QUALITY, + ) + .expect("reference should resize"); + let parsed = parse_image_data_url(resized.as_str()).expect("resized data url should parse"); + let resized_image = + image::load_from_memory(parsed.bytes.as_slice()).expect("resized image should decode"); + let (width, height) = resized_image.dimensions(); + + assert!(width <= OPENING_CG_REFERENCE_MAX_EDGE); + assert!(height <= OPENING_CG_REFERENCE_MAX_EDGE); + assert_eq!(parsed.mime_type, "image/jpeg"); + } + #[test] fn push_cover_reference_source_keeps_full_data_url() { let mut sources = Vec::new(); diff --git a/server-rs/crates/api-server/src/edutainment_baby_object.rs b/server-rs/crates/api-server/src/edutainment_baby_object.rs index 0458a4e6..5fcd8687 100644 --- a/server-rs/crates/api-server/src/edutainment_baby_object.rs +++ b/server-rs/crates/api-server/src/edutainment_baby_object.rs @@ -1049,6 +1049,7 @@ mod tests { base_url: "https://vector.example".to_string(), api_key: "secret".to_string(), request_timeout_ms: 180_000, + external_api_audit_state: None, }); assert_eq!( diff --git a/server-rs/crates/api-server/src/external_api_audit.rs b/server-rs/crates/api-server/src/external_api_audit.rs new file mode 100644 index 00000000..2c609792 --- /dev/null +++ b/server-rs/crates/api-server/src/external_api_audit.rs @@ -0,0 +1,372 @@ +use axum::http::StatusCode; +use module_runtime::RuntimeTrackingScopeKind; +use serde_json::{Value, json}; +use time::OffsetDateTime; +use uuid::Uuid; + +use crate::{state::AppState, tracking::TrackingEventDraft}; + +pub(crate) const EXTERNAL_API_FAILURE_EVENT_KEY: &str = "external_api_call_failure"; +pub(crate) const EXTERNAL_API_AUDIT_MODULE_KEY: &str = "external-api"; + +#[derive(Clone, Debug)] +pub(crate) struct ExternalApiFailureDraft { + pub(crate) provider: &'static str, + pub(crate) endpoint: String, + pub(crate) operation: String, + pub(crate) failure_stage: &'static str, + pub(crate) status_code: Option, + pub(crate) status_class: Option<&'static str>, + pub(crate) timeout: bool, + pub(crate) retryable: bool, + pub(crate) error_message: String, + pub(crate) error_source: Option, + pub(crate) raw_excerpt: Option, + pub(crate) latency_ms: Option, + pub(crate) prompt_chars: Option, + pub(crate) reference_image_count: Option, + pub(crate) image_model: Option<&'static str>, +} + +impl ExternalApiFailureDraft { + pub(crate) fn new( + provider: &'static str, + endpoint: impl Into, + operation: impl Into, + failure_stage: &'static str, + error_message: impl Into, + ) -> Self { + Self { + provider, + endpoint: endpoint.into(), + operation: operation.into(), + failure_stage, + status_code: None, + status_class: None, + timeout: false, + retryable: false, + error_message: error_message.into(), + error_source: None, + raw_excerpt: None, + latency_ms: None, + prompt_chars: None, + reference_image_count: None, + image_model: None, + } + } + + pub(crate) fn with_status_code(mut self, status_code: Option) -> Self { + self.status_code = status_code; + self + } + + pub(crate) fn with_optional_status_class(mut self, status_class: Option<&'static str>) -> Self { + self.status_class = status_class; + self + } + + pub(crate) fn with_timeout(mut self, timeout: bool) -> Self { + self.timeout = timeout; + self + } + + pub(crate) fn with_retryable(mut self, retryable: bool) -> Self { + self.retryable = retryable; + self + } + + pub(crate) fn with_error_source(mut self, error_source: Option) -> Self { + self.error_source = error_source; + self + } + + pub(crate) fn with_raw_excerpt(mut self, raw_excerpt: Option) -> Self { + self.raw_excerpt = raw_excerpt; + self + } + + pub(crate) fn with_latency_ms(mut self, latency_ms: Option) -> Self { + self.latency_ms = latency_ms; + self + } + + pub(crate) fn with_prompt_chars(mut self, prompt_chars: Option) -> Self { + self.prompt_chars = prompt_chars; + self + } + + pub(crate) fn with_reference_image_count( + mut self, + reference_image_count: Option, + ) -> Self { + self.reference_image_count = reference_image_count; + self + } + + pub(crate) fn with_image_model(mut self, image_model: Option<&'static str>) -> Self { + self.image_model = image_model; + self + } +} + +/// 中文注释:下载图片、OSS 读写等非标准 HTTP 状态统一显式归类,避免 OTLP 低基数 label 误落到 `transport`。 +pub(crate) fn app_error_status_class(status_code: StatusCode) -> &'static str { + status_class(Some(status_code.as_u16())) +} + +/// 中文注释:外部供应商失败同时进入 OTLP 和 tracking_event;失败审计不能反向阻断主业务错误返回。 +pub(crate) async fn record_external_api_failure(state: &AppState, draft: ExternalApiFailureDraft) { + record_external_api_failure_otlp(&draft); + + let tracking_event = build_external_api_failure_tracking_draft(&draft); + if let Some(outbox) = state.tracking_outbox() { + match outbox + .enqueue(crate::tracking::build_tracking_event_input( + tracking_event.clone(), + )) + .await + { + Ok(crate::tracking_outbox::TrackingOutboxEnqueueOutcome::Enqueued) => {} + Ok(crate::tracking_outbox::TrackingOutboxEnqueueOutcome::Dropped { reason }) => { + tracing::warn!( + provider = draft.provider, + endpoint = %draft.endpoint, + operation = %draft.operation, + failure_stage = draft.failure_stage, + reason, + "外部 API 失败审计写入 outbox 被保护阈值拒绝,回退同步直写 SpacetimeDB" + ); + crate::tracking::record_tracking_event_after_success( + state, + &audit_request_context(), + tracking_event, + ) + .await; + } + Err(error) => { + tracing::warn!( + provider = draft.provider, + endpoint = %draft.endpoint, + operation = %draft.operation, + failure_stage = draft.failure_stage, + error = %error, + "外部 API 失败审计写入 outbox 失败,回退同步直写 SpacetimeDB" + ); + crate::tracking::record_tracking_event_after_success( + state, + &audit_request_context(), + tracking_event, + ) + .await; + } + } + return; + } + + crate::tracking::record_tracking_event_after_success( + state, + &audit_request_context(), + tracking_event, + ) + .await; +} + +pub(crate) fn build_external_api_failure_tracking_draft( + failure: &ExternalApiFailureDraft, +) -> TrackingEventDraft { + let mut draft = TrackingEventDraft::new( + EXTERNAL_API_FAILURE_EVENT_KEY, + EXTERNAL_API_AUDIT_MODULE_KEY, + ); + draft.scope_kind = RuntimeTrackingScopeKind::Module; + draft.scope_id = failure.provider.to_string(); + draft.metadata = build_external_api_failure_metadata(failure); + draft +} + +fn build_external_api_failure_metadata(failure: &ExternalApiFailureDraft) -> Value { + let mut metadata = json!({ + "provider": failure.provider, + "endpoint": failure.endpoint, + "operation": failure.operation, + "failureStage": failure.failure_stage, + "statusCode": failure.status_code, + "statusClass": failure.status_class.unwrap_or_else(|| status_class(failure.status_code)), + "timeout": failure.timeout, + "retryable": failure.retryable, + "errorMessage": truncate_field(failure.error_message.as_str(), 1_000), + "occurredAt": current_utc_iso_text(), + }); + + if let Some(latency_ms) = failure.latency_ms { + metadata["latencyMs"] = json!(latency_ms); + } + if let Some(prompt_chars) = failure.prompt_chars { + metadata["promptChars"] = json!(prompt_chars); + } + if let Some(reference_image_count) = failure.reference_image_count { + metadata["referenceImageCount"] = json!(reference_image_count); + } + if let Some(image_model) = failure.image_model { + metadata["imageModel"] = json!(image_model); + } + if let Some(source) = failure + .error_source + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + { + metadata["errorSource"] = json!(truncate_field(source, 1_000)); + } + if let Some(excerpt) = failure + .raw_excerpt + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + { + metadata["rawExcerpt"] = json!(truncate_field(excerpt, 800)); + } + + metadata +} + +pub(crate) fn is_retryable_external_api_failure( + status_code: Option, + timeout: bool, + connect: bool, +) -> bool { + timeout + || connect + || status_code.is_some_and(|status| { + status == StatusCode::TOO_MANY_REQUESTS.as_u16() + || status == StatusCode::REQUEST_TIMEOUT.as_u16() + || status >= 500 + }) +} + +fn record_external_api_failure_otlp(failure: &ExternalApiFailureDraft) { + crate::telemetry::record_external_api_failure( + failure.provider, + failure.failure_stage, + failure + .status_class + .unwrap_or_else(|| status_class(failure.status_code)), + failure.retryable, + ); + + tracing::error!( + provider = failure.provider, + endpoint = %failure.endpoint, + operation = %failure.operation, + failure_stage = failure.failure_stage, + status_code = failure.status_code, + status_class = failure.status_class.unwrap_or_else(|| status_class(failure.status_code)), + timeout = failure.timeout, + retryable = failure.retryable, + latency_ms = failure.latency_ms, + prompt_chars = failure.prompt_chars, + reference_image_count = failure.reference_image_count, + image_model = failure.image_model, + error = %failure.error_message, + "外部 API 调用失败" + ); +} + +fn status_class(status_code: Option) -> &'static str { + match status_code { + Some(100..=199) => "1xx", + Some(200..=299) => "2xx", + Some(300..=399) => "3xx", + Some(400..=499) => "4xx", + Some(500..=599) => "5xx", + Some(_) => "unknown", + None => "transport", + } +} + +fn audit_request_context() -> crate::request_context::RequestContext { + crate::request_context::RequestContext::new( + format!("external-api-audit-{}", Uuid::new_v4()), + "external-api audit".to_string(), + std::time::Duration::ZERO, + false, + ) +} + +fn truncate_field(value: &str, max_chars: usize) -> String { + value.chars().take(max_chars).collect() +} + +fn current_utc_iso_text() -> String { + shared_kernel::format_rfc3339(OffsetDateTime::now_utc()) + .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string()) +} + +#[cfg(test)] +mod tests { + use serde_json::Value; + + use super::*; + + #[test] + fn external_api_failure_tracking_draft_uses_module_scope_and_safe_metadata() { + let draft = build_external_api_failure_tracking_draft( + &ExternalApiFailureDraft::new( + "vector-engine", + "https://vector.example/v1/images/generations", + "拼图 UI 背景图生成失败", + "upstream_status", + "上游 429", + ) + .with_status_code(Some(429)) + .with_retryable(true) + .with_latency_ms(Some(1234)) + .with_prompt_chars(Some(88)) + .with_reference_image_count(Some(2)) + .with_image_model(Some("gpt-image-2-all")), + ); + + assert_eq!(draft.event_key, EXTERNAL_API_FAILURE_EVENT_KEY); + assert_eq!(draft.scope_kind, RuntimeTrackingScopeKind::Module); + assert_eq!(draft.scope_id, "vector-engine"); + assert_eq!(draft.module_key, Some(EXTERNAL_API_AUDIT_MODULE_KEY)); + + let metadata = draft.metadata; + assert_eq!(metadata["provider"], "vector-engine"); + assert_eq!(metadata["statusCode"], 429); + assert_eq!(metadata["statusClass"], "4xx"); + assert_eq!(metadata["retryable"], true); + assert_eq!(metadata["latencyMs"], 1234); + assert_eq!(metadata["promptChars"], 88); + assert_eq!(metadata["referenceImageCount"], 2); + assert_eq!(metadata["imageModel"], "gpt-image-2-all"); + assert!(matches!(metadata["occurredAt"], Value::String(_))); + } + + #[test] + fn retryable_classification_keeps_transport_and_overload_failures_actionable() { + assert!(is_retryable_external_api_failure(None, true, false)); + assert!(is_retryable_external_api_failure(None, false, true)); + assert!(is_retryable_external_api_failure(Some(429), false, false)); + assert!(is_retryable_external_api_failure(Some(502), false, false)); + assert!(!is_retryable_external_api_failure(Some(400), false, false)); + } + + #[test] + fn app_error_status_class_can_override_successful_upstream_status() { + let draft = build_external_api_failure_tracking_draft( + &ExternalApiFailureDraft::new( + "vector-engine", + "https://cdn.example/generated.png", + "下载生成图片", + "image_download", + "下载生成图片失败", + ) + .with_status_code(Some(200)) + .with_optional_status_class(Some(app_error_status_class(StatusCode::BAD_GATEWAY))), + ); + + assert_eq!(draft.metadata["statusCode"], 200); + assert_eq!(draft.metadata["statusClass"], "5xx"); + } +} diff --git a/server-rs/crates/api-server/src/generated_asset_sheets.rs b/server-rs/crates/api-server/src/generated_asset_sheets.rs new file mode 100644 index 00000000..18c1423b --- /dev/null +++ b/server-rs/crates/api-server/src/generated_asset_sheets.rs @@ -0,0 +1,1745 @@ +#![allow(dead_code)] + +use std::{collections::BTreeMap, time::Duration}; + +use axum::http::StatusCode; +use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; +use image::{GenericImageView, ImageFormat}; +use platform_oss::{LegacyAssetPrefix, OssObjectAccess, OssPutObjectRequest}; +use serde_json::json; + +use crate::{ + http_error::AppError, openai_image_generation::DownloadedOpenAiImage, + platform_errors::map_oss_error, state::AppState, +}; + +const GENERATED_ASSET_SHEET_PROVIDER: &str = "generated-asset-sheets"; +const GENERATED_ASSET_SHEET_OSS_PUT_TIMEOUT_MS: u64 = 3 * 60_000; +const GENERATED_ASSET_SHEET_FOREGROUND_DIFF_THRESHOLD: i32 = 36; +const GENERATED_ASSET_SHEET_FOREGROUND_ALPHA_THRESHOLD: i32 = 36; +const GENERATED_ASSET_SHEET_GREEN_SCREEN_MIN_SCORE: f32 = 0.34; +const GENERATED_ASSET_SHEET_GREEN_SCREEN_SOFT_SCORE: f32 = 0.18; +const GENERATED_ASSET_SHEET_HARD_GREEN_SCREEN_SCORE: f32 = 0.82; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct GeneratedAssetSheetPromptInput<'a> { + pub(crate) subject_text: &'a str, + pub(crate) item_names: &'a [String], + pub(crate) grid_size: usize, + pub(crate) item_name_prompt_template: Option<&'a str>, + pub(crate) special_prompt: Option<&'a str>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct GeneratedAssetSheetSliceImage { + pub(crate) bytes: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct GeneratedAssetSheetUpload { + pub(crate) src: String, + pub(crate) object_key: String, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(crate) struct GeneratedAssetSheetPersistPrompt { + pub(crate) sheet_prompt: Option, + pub(crate) item_name_prompt: Option, + pub(crate) special_prompt: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct GeneratedAssetSheetPersistInput { + pub(crate) prefix: LegacyAssetPrefix, + pub(crate) owner_user_id: String, + pub(crate) session_id: String, + pub(crate) profile_id: String, + pub(crate) path_segments: Vec, + pub(crate) file_name: String, + pub(crate) content_type: String, + pub(crate) bytes: Vec, + pub(crate) asset_kind: String, + pub(crate) source_job_id: Option, + pub(crate) generated_at_micros: i64, + pub(crate) grid_size: usize, + pub(crate) row_index: usize, + pub(crate) view_index: usize, + pub(crate) prompt: GeneratedAssetSheetPersistPrompt, +} + +pub(crate) fn build_generated_asset_sheet_prompt( + input: &GeneratedAssetSheetPromptInput<'_>, +) -> Result { + let grid_size = input.grid_size; + if grid_size == 0 { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": GENERATED_ASSET_SHEET_PROVIDER, + "message": "系列素材图集的 n 必须大于 0。", + })), + ); + } + if input.item_names.len() > grid_size { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": GENERATED_ASSET_SHEET_PROVIDER, + "message": "系列素材图集的物品行数不能超过 n。", + "gridSize": grid_size, + "itemCount": input.item_names.len(), + })), + ); + } + + let subject_text = input.subject_text.trim(); + let subject_text = if subject_text.is_empty() { + "系列素材" + } else { + subject_text + }; + let item_rows = input + .item_names + .iter() + .enumerate() + .map(|(index, item_name)| { + let row_index = index + 1; + let item_name = item_name.trim(); + if let Some(template) = input + .item_name_prompt_template + .map(str::trim) + .filter(|value| !value.is_empty()) + { + return template + .replace("{row_index}", row_index.to_string().as_str()) + .replace("{item_name}", item_name) + .replace("{view_count}", grid_size.to_string().as_str()); + } + format!("第{row_index}行:{item_name} 的 {grid_size} 个不同视图") + }) + .collect::>() + .join(";"); + let special_prompt = input + .special_prompt + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(str::to_string) + .unwrap_or_else(|| format!("每个物品生成 {grid_size} 个不同视图。")); + + Ok(format!( + "生成一张1:1图片。固定生成{grid_size}行*{grid_size}列网格素材图,画面是{subject_text}。严格{grid_size}*{grid_size}均匀排布,严格按行组织:{item_rows}。{special_prompt}每个格子一个独立居中的完整素材,每格背景必须是统一纯绿色绿幕背景(高饱和亮绿色,接近 #00FF00),背景平整无纹理、无渐变、无阴影、无道具,方便后续抠成透明。素材本身不得使用与绿幕相同的纯绿色;若素材天然含绿色,必须使用更深、更黄或更蓝的绿色并用清晰描边与绿幕区分。统一柔和光照,清晰轮廓,适合直接切割成游戏2D素材。请让每个素材完整落在自己的格子中央,四周保留留白,相邻素材主体之间必须至少保留单个素材格宽度的1/4空白间距(约25%单格宽度),包含左右相邻格和上下相邻行,素材主体不得占满格子。禁止主体跨格、贴边或越界,禁止任何内容进入相邻格子影响裁剪后的效果。不要出现文字、水印、UI、边框、网格线、标签、底座、场景或其他物体。" + )) +} + +pub(crate) fn slice_generated_asset_sheet( + image: &DownloadedOpenAiImage, + item_names: &[String], + grid_size: usize, +) -> Result>, AppError> { + if grid_size == 0 { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": GENERATED_ASSET_SHEET_PROVIDER, + "message": "系列素材图集的 n 必须大于 0。", + })), + ); + } + + let grid_size_u32 = u32::try_from(grid_size).map_err(|_| { + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": GENERATED_ASSET_SHEET_PROVIDER, + "message": "系列素材图集的 n 超出可支持范围。", + })) + })?; + let source = image::load_from_memory(image.bytes.as_slice()).map_err(|error| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": GENERATED_ASSET_SHEET_PROVIDER, + "message": format!("系列素材图集解码失败:{error}"), + })) + })?; + let source = apply_generated_asset_sheet_green_screen_alpha(source); + let (width, height) = source.dimensions(); + let cell_width = width / grid_size_u32; + let cell_height = height / grid_size_u32; + if cell_width == 0 || cell_height == 0 { + return Err( + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": GENERATED_ASSET_SHEET_PROVIDER, + "message": "系列素材图集尺寸过小,无法切割。", + })), + ); + } + + let mut slices = Vec::with_capacity(item_names.len().min(grid_size)); + for item_index in 0..item_names.len().min(grid_size) { + let row = item_index as u32; + let mut views = Vec::with_capacity(grid_size); + for view_index in 0..grid_size { + let col = view_index as u32; + let (crop_x, crop_y, crop_width, crop_height) = + resolve_generated_asset_sheet_cell_crop(&source, grid_size_u32, row, col); + let cropped = source.crop_imm(crop_x, crop_y, crop_width, crop_height); + let cleaned = crop_generated_asset_sheet_view_edge_matte(cropped); + let mut cursor = std::io::Cursor::new(Vec::new()); + cleaned + .write_to(&mut cursor, ImageFormat::Png) + .map_err(|error| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": GENERATED_ASSET_SHEET_PROVIDER, + "message": format!("系列素材图集切割失败:{error}"), + })) + })?; + views.push(GeneratedAssetSheetSliceImage { + bytes: cursor.into_inner(), + }); + } + slices.push(views); + } + + Ok(slices) +} + +pub(crate) fn slice_generated_asset_sheet_two_items_per_row( + image: &DownloadedOpenAiImage, + item_names: &[String], + grid_size: usize, + views_per_item: usize, +) -> Result>, AppError> { + if grid_size == 0 || views_per_item == 0 { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": GENERATED_ASSET_SHEET_PROVIDER, + "message": "系列素材图集的 n 和每物品视图数必须大于 0。", + })), + ); + } + if !grid_size.is_multiple_of(views_per_item) { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": GENERATED_ASSET_SHEET_PROVIDER, + "message": "系列素材图集每行必须能均分为若干物品。", + "gridSize": grid_size, + "viewsPerItem": views_per_item, + })), + ); + } + + let grid_size_u32 = u32::try_from(grid_size).map_err(|_| { + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": GENERATED_ASSET_SHEET_PROVIDER, + "message": "系列素材图集的 n 超出可支持范围。", + })) + })?; + let source = image::load_from_memory(image.bytes.as_slice()).map_err(|error| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": GENERATED_ASSET_SHEET_PROVIDER, + "message": format!("系列素材图集解码失败:{error}"), + })) + })?; + let source = apply_generated_asset_sheet_green_screen_alpha(source); + let (width, height) = source.dimensions(); + if width / grid_size_u32 == 0 || height / grid_size_u32 == 0 { + return Err( + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": GENERATED_ASSET_SHEET_PROVIDER, + "message": "系列素材图集尺寸过小,无法切割。", + })), + ); + } + + let items_per_row = grid_size / views_per_item; + let max_item_count = grid_size.saturating_mul(items_per_row); + let mut slices = Vec::with_capacity(item_names.len().min(max_item_count)); + for item_index in 0..item_names.len().min(max_item_count) { + let row = (item_index / items_per_row) as u32; + let start_col = ((item_index % items_per_row) * views_per_item) as u32; + let mut views = Vec::with_capacity(views_per_item); + for view_offset in 0..views_per_item { + let col = start_col + view_offset as u32; + let (crop_x, crop_y, crop_width, crop_height) = + resolve_generated_asset_sheet_cell_crop(&source, grid_size_u32, row, col); + let cropped = source.crop_imm(crop_x, crop_y, crop_width, crop_height); + let cleaned = crop_generated_asset_sheet_view_edge_matte(cropped); + let mut cursor = std::io::Cursor::new(Vec::new()); + cleaned + .write_to(&mut cursor, ImageFormat::Png) + .map_err(|error| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": GENERATED_ASSET_SHEET_PROVIDER, + "message": format!("系列素材图集切割失败:{error}"), + })) + })?; + views.push(GeneratedAssetSheetSliceImage { + bytes: cursor.into_inner(), + }); + } + slices.push(views); + } + + Ok(slices) +} + +pub(crate) fn crop_generated_asset_sheet_view_edge_matte( + image: image::DynamicImage, +) -> image::DynamicImage { + let mut image = image.to_rgba8(); + let (width, height) = image.dimensions(); + remove_generated_asset_sheet_view_edge_matte(image.as_mut(), width as usize, height as usize); + let bounds = detect_generated_asset_sheet_visible_bounds(&image).unwrap_or_else(|| { + GeneratedAssetSheetCellBounds { + x0: 0, + y0: 0, + x1: width, + y1: height, + } + }); + if bounds.x0 == 0 && bounds.y0 == 0 && bounds.x1 == width && bounds.y1 == height { + return image::DynamicImage::ImageRgba8(image); + } + + image::DynamicImage::ImageRgba8( + image::imageops::crop_imm( + &image, + bounds.x0, + bounds.y0, + bounds.width(), + bounds.height(), + ) + .to_image(), + ) +} + +pub(crate) fn prepare_generated_asset_sheet_put_request( + input: GeneratedAssetSheetPersistInput, +) -> Result { + if input.grid_size == 0 { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": GENERATED_ASSET_SHEET_PROVIDER, + "message": "系列素材图集的 n 必须大于 0。", + })), + ); + } + if input.row_index == 0 + || input.view_index == 0 + || input.row_index > input.grid_size + || input.view_index > input.grid_size + { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": GENERATED_ASSET_SHEET_PROVIDER, + "message": "系列素材图集持久化的行列索引必须落在 n*n 范围内。", + "gridSize": input.grid_size, + "rowIndex": input.row_index, + "viewIndex": input.view_index, + })), + ); + } + + let mut metadata = BTreeMap::new(); + metadata.insert( + "x-oss-meta-asset-kind".to_string(), + input.asset_kind.clone(), + ); + metadata.insert( + "x-oss-meta-owner-user-id".to_string(), + input.owner_user_id.clone(), + ); + metadata.insert( + "x-oss-meta-profile-id".to_string(), + input.profile_id.clone(), + ); + metadata.insert( + "x-oss-meta-generated-asset-sheet-grid-size".to_string(), + input.grid_size.to_string(), + ); + metadata.insert( + "x-oss-meta-generated-asset-sheet-row-index".to_string(), + input.row_index.to_string(), + ); + metadata.insert( + "x-oss-meta-generated-asset-sheet-view-index".to_string(), + input.view_index.to_string(), + ); + metadata.insert( + "x-oss-meta-generated-at-micros".to_string(), + input.generated_at_micros.to_string(), + ); + if let Some(source_job_id) = input + .source_job_id + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + { + metadata.insert( + "x-oss-meta-source-job-id".to_string(), + source_job_id.to_string(), + ); + } + insert_generated_asset_sheet_prompt_metadata( + &mut metadata, + "generated-asset-sheet-prompt-b64", + input.prompt.sheet_prompt.as_deref(), + ); + insert_generated_asset_sheet_prompt_metadata( + &mut metadata, + "generated-asset-sheet-item-name-prompt-b64", + input.prompt.item_name_prompt.as_deref(), + ); + insert_generated_asset_sheet_prompt_metadata( + &mut metadata, + "generated-asset-sheet-special-prompt-b64", + input.prompt.special_prompt.as_deref(), + ); + if input.prompt.sheet_prompt.is_some() + || input.prompt.item_name_prompt.is_some() + || input.prompt.special_prompt.is_some() + { + metadata.insert( + "x-oss-meta-generated-asset-sheet-prompt-encoding".to_string(), + "utf8-base64".to_string(), + ); + } + + Ok(OssPutObjectRequest { + prefix: input.prefix, + path_segments: std::iter::once(input.session_id.as_str()) + .chain(std::iter::once(input.profile_id.as_str())) + .chain(input.path_segments.iter().map(String::as_str)) + .map(|segment| sanitize_generated_asset_sheet_path_segment(segment, "asset")) + .collect(), + file_name: input.file_name, + content_type: Some(input.content_type), + access: OssObjectAccess::Private, + metadata, + body: input.bytes, + }) +} + +pub(crate) async fn persist_generated_asset_sheet_bytes( + state: &AppState, + input: GeneratedAssetSheetPersistInput, +) -> Result { + let oss_client = state.oss_client().ok_or_else(|| { + AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({ + "provider": "aliyun-oss", + "reason": "OSS 未完成环境变量配置", + })) + })?; + let put_request = prepare_generated_asset_sheet_put_request(input)?; + let oss_http_client = reqwest::Client::builder() + .timeout(Duration::from_millis( + GENERATED_ASSET_SHEET_OSS_PUT_TIMEOUT_MS, + )) + .build() + .map_err(|error| { + AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({ + "provider": GENERATED_ASSET_SHEET_PROVIDER, + "message": format!("构造系列素材图集 OSS 上传客户端失败:{error}"), + })) + })?; + let put_result = oss_client + .put_object(&oss_http_client, put_request) + .await + .map_err(|error| map_oss_error(error, "aliyun-oss"))?; + + Ok(GeneratedAssetSheetUpload { + src: put_result.legacy_public_path, + object_key: put_result.object_key, + }) +} + +fn insert_generated_asset_sheet_prompt_metadata( + metadata: &mut BTreeMap, + key: &str, + value: Option<&str>, +) { + let Some(value) = value.map(str::trim).filter(|value| !value.is_empty()) else { + return; + }; + metadata.insert( + format!("x-oss-meta-{key}"), + BASE64_STANDARD.encode(value.as_bytes()), + ); +} + +#[derive(Clone, Copy, Debug)] +struct GeneratedAssetSheetCellBounds { + x0: u32, + y0: u32, + x1: u32, + y1: u32, +} + +impl GeneratedAssetSheetCellBounds { + fn width(self) -> u32 { + self.x1.saturating_sub(self.x0).max(1) + } + + fn height(self) -> u32 { + self.y1.saturating_sub(self.y0).max(1) + } + + fn area(self) -> u32 { + self.width().saturating_mul(self.height()) + } + + fn to_crop_tuple(self) -> (u32, u32, u32, u32) { + (self.x0, self.y0, self.width(), self.height()) + } +} + +fn resolve_generated_asset_sheet_cell_crop( + source: &image::DynamicImage, + grid_size: u32, + row: u32, + col: u32, +) -> (u32, u32, u32, u32) { + let (image_width, image_height) = source.dimensions(); + let cell = + resolve_generated_asset_sheet_cell_bounds(image_width, image_height, grid_size, row, col); + let Some(foreground) = detect_generated_asset_sheet_foreground_bounds(source, cell) else { + return cell.to_crop_tuple(); + }; + + let cell_width = cell.width(); + let cell_height = cell.height(); + let pad_x = (cell_width / 16).clamp(4, 16); + let pad_y = (cell_height / 16).clamp(4, 16); + let crop = GeneratedAssetSheetCellBounds { + x0: foreground.x0.saturating_sub(pad_x).max(cell.x0), + y0: foreground.y0.saturating_sub(pad_y).max(cell.y0), + x1: foreground.x1.saturating_add(pad_x).min(cell.x1), + y1: foreground.y1.saturating_add(pad_y).min(cell.y1), + }; + + crop.to_crop_tuple() +} + +fn resolve_generated_asset_sheet_cell_bounds( + image_width: u32, + image_height: u32, + grid_size: u32, + row: u32, + col: u32, +) -> GeneratedAssetSheetCellBounds { + let normalized_grid_size = grid_size.max(1); + let cell_x0 = col.saturating_mul(image_width) / normalized_grid_size; + let cell_x1 = (col.saturating_add(1)).saturating_mul(image_width) / normalized_grid_size; + let cell_y0 = row.saturating_mul(image_height) / normalized_grid_size; + let cell_y1 = (row.saturating_add(1)).saturating_mul(image_height) / normalized_grid_size; + + GeneratedAssetSheetCellBounds { + x0: cell_x0.min(image_width.saturating_sub(1)), + y0: cell_y0.min(image_height.saturating_sub(1)), + x1: cell_x1.clamp(cell_x0.saturating_add(1), image_width), + y1: cell_y1.clamp(cell_y0.saturating_add(1), image_height), + } +} + +fn detect_generated_asset_sheet_foreground_bounds( + source: &image::DynamicImage, + cell: GeneratedAssetSheetCellBounds, +) -> Option { + let background = sample_generated_asset_sheet_cell_background(source, cell); + let mut foreground: Option = None; + let mut foreground_pixels = 0u32; + + for y in cell.y0..cell.y1 { + for x in cell.x0..cell.x1 { + if !is_generated_asset_sheet_foreground_pixel(source.get_pixel(x, y).0, background) { + continue; + } + foreground_pixels = foreground_pixels.saturating_add(1); + foreground = Some(match foreground { + Some(bounds) => GeneratedAssetSheetCellBounds { + x0: bounds.x0.min(x), + y0: bounds.y0.min(y), + x1: bounds.x1.max(x.saturating_add(1)), + y1: bounds.y1.max(y.saturating_add(1)), + }, + None => GeneratedAssetSheetCellBounds { + x0: x, + y0: y, + x1: x.saturating_add(1), + y1: y.saturating_add(1), + }, + }); + } + } + + let min_foreground_pixels = (cell.area() / 320).clamp(12, 220); + foreground.filter(|bounds| { + foreground_pixels >= min_foreground_pixels && bounds.width() > 2 && bounds.height() > 2 + }) +} + +fn detect_generated_asset_sheet_visible_bounds( + image: &image::RgbaImage, +) -> Option { + let (width, height) = image.dimensions(); + let mut bounds: Option = None; + let mut visible_pixels = 0u32; + + for y in 0..height { + for x in 0..width { + let pixel = image.get_pixel(x, y).0; + if !is_generated_asset_sheet_visible_pixel(pixel) { + continue; + } + visible_pixels = visible_pixels.saturating_add(1); + bounds = Some(match bounds { + Some(current) => GeneratedAssetSheetCellBounds { + x0: current.x0.min(x), + y0: current.y0.min(y), + x1: current.x1.max(x.saturating_add(1)), + y1: current.y1.max(y.saturating_add(1)), + }, + None => GeneratedAssetSheetCellBounds { + x0: x, + y0: y, + x1: x.saturating_add(1), + y1: y.saturating_add(1), + }, + }); + } + } + + let min_visible_pixels = ((width.saturating_mul(height)) / 540).clamp(10, 120); + bounds.filter(|visible_bounds| { + visible_pixels >= min_visible_pixels + && visible_bounds.width() > 2 + && visible_bounds.height() > 2 + }) +} + +fn sample_generated_asset_sheet_cell_background( + source: &image::DynamicImage, + cell: GeneratedAssetSheetCellBounds, +) -> [u8; 4] { + let sample_size = (cell.width().min(cell.height()) / 12).clamp(2, 8); + let sample_points = [ + (cell.x0, cell.y0), + (cell.x1.saturating_sub(sample_size), cell.y0), + (cell.x0, cell.y1.saturating_sub(sample_size)), + ( + cell.x1.saturating_sub(sample_size), + cell.y1.saturating_sub(sample_size), + ), + ]; + let mut samples = Vec::new(); + for (start_x, start_y) in sample_points { + let mut totals = [0u32; 4]; + let mut count = 0u32; + for y in start_y..start_y.saturating_add(sample_size).min(cell.y1) { + for x in start_x..start_x.saturating_add(sample_size).min(cell.x1) { + let pixel = source.get_pixel(x, y).0; + totals[0] = totals[0].saturating_add(pixel[0] as u32); + totals[1] = totals[1].saturating_add(pixel[1] as u32); + totals[2] = totals[2].saturating_add(pixel[2] as u32); + totals[3] = totals[3].saturating_add(pixel[3] as u32); + count = count.saturating_add(1); + } + } + if count > 0 { + samples.push([ + (totals[0] / count) as u8, + (totals[1] / count) as u8, + (totals[2] / count) as u8, + (totals[3] / count) as u8, + ]); + } + } + + samples + .into_iter() + .min_by_key(|sample| { + let luminance = sample[0] as u16 + sample[1] as u16 + sample[2] as u16; + (sample[3] as u16, u16::MAX.saturating_sub(luminance)) + }) + .unwrap_or([255, 255, 255, 255]) +} + +fn clamp_generated_asset_sheet_unit(value: f32) -> f32 { + value.clamp(0.0, 1.0) +} + +fn lerp_generated_asset_sheet_channel(from: f32, to: f32, t: f32) -> f32 { + from + (to - from) * clamp_generated_asset_sheet_unit(t) +} + +fn is_generated_asset_sheet_foreground_pixel(pixel: [u8; 4], background: [u8; 4]) -> bool { + let alpha_diff = pixel[3] as i32 - background[3] as i32; + if alpha_diff.abs() >= GENERATED_ASSET_SHEET_FOREGROUND_ALPHA_THRESHOLD && pixel[3] > 24 { + return true; + } + if pixel[3] <= 24 { + return false; + } + + let color_diff = (pixel[0] as i32 - background[0] as i32).abs() + + (pixel[1] as i32 - background[1] as i32).abs() + + (pixel[2] as i32 - background[2] as i32).abs(); + color_diff >= GENERATED_ASSET_SHEET_FOREGROUND_DIFF_THRESHOLD +} + +fn remove_generated_asset_sheet_view_edge_matte( + pixels: &mut [u8], + width: usize, + height: usize, +) -> bool { + let pixel_count = width.saturating_mul(height); + if pixel_count == 0 || pixels.len() < pixel_count.saturating_mul(4) { + return false; + } + + let mut changed = false; + let mut background_mask = vec![0u8; pixel_count]; + let mut queue = Vec::::new(); + let mut queue_index = 0usize; + let mut transparent_pixel_count = 0usize; + for pixel_index in 0..pixel_count { + let offset = pixel_index * 4; + if pixels[offset + 3] == 0 { + background_mask[pixel_index] = 1; + queue.push(pixel_index); + transparent_pixel_count = transparent_pixel_count.saturating_add(1); + } + } + let has_transparent_background = transparent_pixel_count > pixel_count / 200; + + // 中文注释:单图被前景边界收紧后,浅绿框可能正好贴在 PNG 外缘; + // 把外缘一段宽度作为去背种子,但只清理绿幕 / 近白 matte,避免误伤贴边主体。 + let edge_width = resolve_generated_asset_sheet_view_edge_cleanup_width(width, height); + for y in 0..height { + for x in 0..width { + if x >= edge_width + && y >= edge_width + && x.saturating_add(edge_width) < width + && y.saturating_add(edge_width) < height + { + continue; + } + let pixel_index = y * width + x; + if background_mask[pixel_index] != 0 { + continue; + } + let offset = pixel_index * 4; + let pixel = [ + pixels[offset], + pixels[offset + 1], + pixels[offset + 2], + pixels[offset + 3], + ]; + if !is_generated_asset_sheet_view_background_pixel(pixel) { + continue; + } + background_mask[pixel_index] = 1; + queue.push(pixel_index); + } + } + + while queue_index < queue.len() { + let pixel_index = queue[queue_index]; + queue_index += 1; + let x = pixel_index % width; + let y = pixel_index / width; + let neighbors = [ + (x > 0).then(|| pixel_index - 1), + (x + 1 < width).then_some(pixel_index + 1), + (y > 0).then(|| pixel_index - width), + (y + 1 < height).then_some(pixel_index + width), + ]; + + for next_pixel_index in neighbors.into_iter().flatten() { + if background_mask[next_pixel_index] != 0 { + continue; + } + let offset = next_pixel_index * 4; + let pixel = [ + pixels[offset], + pixels[offset + 1], + pixels[offset + 2], + pixels[offset + 3], + ]; + if !is_generated_asset_sheet_view_background_pixel(pixel) { + continue; + } + background_mask[next_pixel_index] = 1; + queue.push(next_pixel_index); + } + } + + for _ in 0..edge_width { + let mut expanded_mask = background_mask.clone(); + let mut changed_this_round = false; + for y in 0..height { + for x in 0..width { + let pixel_index = y * width + x; + if background_mask[pixel_index] != 0 { + continue; + } + let offset = pixel_index * 4; + if !is_generated_asset_sheet_view_background_pixel([ + pixels[offset], + pixels[offset + 1], + pixels[offset + 2], + pixels[offset + 3], + ]) { + continue; + } + + if touches_generated_asset_sheet_background_mask( + x, + y, + width, + height, + &background_mask, + ) { + expanded_mask[pixel_index] = 1; + changed_this_round = true; + } + } + } + background_mask = expanded_mask; + if !changed_this_round { + break; + } + } + + for pixel_index in 0..pixel_count { + if background_mask[pixel_index] == 0 { + continue; + } + let offset = pixel_index * 4; + if pixels[offset + 3] != 0 + || pixels[offset] != 0 + || pixels[offset + 1] != 0 + || pixels[offset + 2] != 0 + { + pixels[offset] = 0; + pixels[offset + 1] = 0; + pixels[offset + 2] = 0; + pixels[offset + 3] = 0; + changed = true; + } + } + + if has_transparent_background { + let mut visible_mask = vec![0u8; pixel_count]; + for pixel_index in 0..pixel_count { + let offset = pixel_index * 4; + if is_generated_asset_sheet_visible_pixel([ + pixels[offset], + pixels[offset + 1], + pixels[offset + 2], + pixels[offset + 3], + ]) { + visible_mask[pixel_index] = 1; + } + } + + for _ in 0..2 { + let mut changed_this_round = false; + for y in 0..height { + for x in 0..width { + let pixel_index = y * width + x; + if visible_mask[pixel_index] == 0 { + continue; + } + let offset = pixel_index * 4; + let pixel = [ + pixels[offset], + pixels[offset + 1], + pixels[offset + 2], + pixels[offset + 3], + ]; + if !is_generated_asset_sheet_green_contaminated_edge_pixel(pixel) { + continue; + } + if !touches_generated_asset_sheet_background_mask( + x, + y, + width, + height, + &background_mask, + ) { + continue; + } + + if is_generated_asset_sheet_strong_green_contamination(pixel) { + pixels[offset] = 0; + pixels[offset + 1] = 0; + pixels[offset + 2] = 0; + pixels[offset + 3] = 0; + visible_mask[pixel_index] = 0; + background_mask[pixel_index] = 1; + changed = true; + changed_this_round = true; + continue; + } + + let replacement = collect_generated_asset_sheet_visible_neighbor_color( + pixels, + width, + height, + x, + y, + &background_mask, + &visible_mask, + ) + .unwrap_or(( + pixels[offset], + pixels[offset + 1], + pixels[offset + 2], + )); + let next_red = replacement.0.max(pixels[offset]); + let next_blue = replacement.2.max(pixels[offset + 2]); + let next_green = replacement + .1 + .min(next_red.max(next_blue).saturating_add(12)); + if next_red != pixels[offset] + || next_green != pixels[offset + 1] + || next_blue != pixels[offset + 2] + { + pixels[offset] = next_red; + pixels[offset + 1] = next_green; + pixels[offset + 2] = next_blue; + changed = true; + changed_this_round = true; + } + background_mask[pixel_index] = 1; + } + } + if !changed_this_round { + break; + } + } + } + + changed +} + +fn resolve_generated_asset_sheet_view_edge_cleanup_width(width: usize, height: usize) -> usize { + let min_side = width.min(height).max(1); + (min_side / 24).clamp(4, 12).min(min_side) +} + +fn is_generated_asset_sheet_view_background_pixel(pixel: [u8; 4]) -> bool { + pixel[3] < 16 + || is_generated_asset_sheet_soft_edge_pixel(pixel) + || compute_generated_asset_sheet_white_screen_score(pixel) > 0.18 +} + +fn is_generated_asset_sheet_visible_pixel(pixel: [u8; 4]) -> bool { + pixel[3] > 0 && (pixel[0] > 8 || pixel[1] > 8 || pixel[2] > 8) +} + +fn is_generated_asset_sheet_soft_edge_pixel(pixel: [u8; 4]) -> bool { + if pixel[3] == 0 { + return false; + } + + let red = pixel[0]; + let green = pixel[1]; + let blue = pixel[2]; + green >= 188 + && green.saturating_sub(red.max(blue)) >= 42 + && (red >= 48 || blue >= 96 || pixel[3] < 236) +} + +fn is_generated_asset_sheet_green_contaminated_edge_pixel(pixel: [u8; 4]) -> bool { + if pixel[3] == 0 { + return false; + } + + let red = pixel[0]; + let green = pixel[1]; + let blue = pixel[2]; + green >= 72 && green.saturating_sub(red.max(blue)) >= 18 +} + +fn is_generated_asset_sheet_strong_green_contamination(pixel: [u8; 4]) -> bool { + let red = pixel[0]; + let green = pixel[1]; + let blue = pixel[2]; + green >= 148 && green.saturating_sub(red.max(blue)) >= 34 +} + +fn collect_generated_asset_sheet_visible_neighbor_color( + pixels: &[u8], + width: usize, + height: usize, + x: usize, + y: usize, + background_mask: &[u8], + visible_mask: &[u8], +) -> Option<(u8, u8, u8)> { + let mut total_weight = 0.0f32; + let mut total_red = 0.0f32; + let mut total_green = 0.0f32; + let mut total_blue = 0.0f32; + + for offset_y in -3i32..=3 { + for offset_x in -3i32..=3 { + if offset_x == 0 && offset_y == 0 { + continue; + } + let next_x = x as i32 + offset_x; + let next_y = y as i32 + offset_y; + if next_x < 0 || next_x >= width as i32 || next_y < 0 || next_y >= height as i32 { + continue; + } + + let next_pixel_index = next_y as usize * width + next_x as usize; + if background_mask[next_pixel_index] != 0 || visible_mask[next_pixel_index] == 0 { + continue; + } + + let next_offset = next_pixel_index * 4; + let next_alpha = pixels[next_offset + 3]; + if next_alpha < 96 { + continue; + } + let pixel = [ + pixels[next_offset], + pixels[next_offset + 1], + pixels[next_offset + 2], + next_alpha, + ]; + if is_generated_asset_sheet_green_contaminated_edge_pixel(pixel) + || is_generated_asset_sheet_soft_edge_pixel(pixel) + { + continue; + } + + let distance = offset_x.unsigned_abs() + offset_y.unsigned_abs(); + let weight = (next_alpha as f32 / 255.0) + * if distance <= 1 { + 2.0 + } else if distance <= 3 { + 1.2 + } else { + 0.7 + }; + total_weight += weight; + total_red += pixels[next_offset] as f32 * weight; + total_green += pixels[next_offset + 1] as f32 * weight; + total_blue += pixels[next_offset + 2] as f32 * weight; + } + } + + if total_weight <= 0.0 { + return None; + } + + Some(( + (total_red / total_weight).round() as u8, + (total_green / total_weight).round() as u8, + (total_blue / total_weight).round() as u8, + )) +} + +pub(crate) fn apply_generated_asset_sheet_green_screen_alpha( + source: image::DynamicImage, +) -> image::DynamicImage { + let mut image = source.to_rgba8(); + let (width, height) = image.dimensions(); + remove_generated_asset_sheet_green_screen_background( + image.as_mut(), + width as usize, + height as usize, + ); + image::DynamicImage::ImageRgba8(image) +} + +fn remove_generated_asset_sheet_green_screen_background( + pixels: &mut [u8], + width: usize, + height: usize, +) -> bool { + let pixel_count = width.saturating_mul(height); + if pixel_count == 0 || pixels.len() < pixel_count.saturating_mul(4) { + return false; + } + + let mut green_scores = vec![0.0f32; pixel_count]; + let mut white_scores = vec![0.0f32; pixel_count]; + let mut background_hints = vec![0.0f32; pixel_count]; + let mut background_mask = vec![0u8; pixel_count]; + let mut queue = Vec::::new(); + let mut queue_index = 0usize; + + for pixel_index in 0..pixel_count { + let offset = pixel_index * 4; + let red = pixels[offset]; + let green = pixels[offset + 1]; + let blue = pixels[offset + 2]; + let alpha = pixels[offset + 3]; + let green_score = + compute_generated_asset_sheet_green_screen_score([red, green, blue, alpha]); + let white_score = + compute_generated_asset_sheet_white_screen_score([red, green, blue, alpha]); + let transparency_hint = + clamp_generated_asset_sheet_unit((56.0 - alpha as f32) / 56.0) * 0.75; + + green_scores[pixel_index] = green_score; + white_scores[pixel_index] = white_score; + background_hints[pixel_index] = green_score.max(white_score).max(transparency_hint); + } + + let seed_background_pixel = + |pixel_index: usize, background_mask: &mut [u8], queue: &mut Vec| { + if background_mask[pixel_index] != 0 { + return; + } + let alpha = pixels[pixel_index * 4 + 3]; + let strong_candidate = alpha < 40 + || green_scores[pixel_index] >= GENERATED_ASSET_SHEET_HARD_GREEN_SCREEN_SCORE + || (alpha < 224 + && green_scores[pixel_index] > GENERATED_ASSET_SHEET_GREEN_SCREEN_MIN_SCORE) + || white_scores[pixel_index] > 0.32; + if !strong_candidate { + return; + } + background_mask[pixel_index] = 1; + queue.push(pixel_index); + }; + + for x in 0..width { + seed_background_pixel(x, &mut background_mask, &mut queue); + seed_background_pixel((height - 1) * width + x, &mut background_mask, &mut queue); + } + for y in 1..height.saturating_sub(1) { + seed_background_pixel(y * width, &mut background_mask, &mut queue); + seed_background_pixel(y * width + width - 1, &mut background_mask, &mut queue); + } + + while queue_index < queue.len() { + let pixel_index = queue[queue_index]; + queue_index += 1; + + let x = pixel_index % width; + let y = pixel_index / width; + let neighbor_indexes = [ + if x > 0 { Some(pixel_index - 1) } else { None }, + if x + 1 < width { + Some(pixel_index + 1) + } else { + None + }, + if y > 0 { + Some(pixel_index - width) + } else { + None + }, + if y + 1 < height { + Some(pixel_index + width) + } else { + None + }, + ]; + + for next_pixel_index in neighbor_indexes.into_iter().flatten() { + if background_mask[next_pixel_index] != 0 { + continue; + } + let next_offset = next_pixel_index * 4; + let alpha = pixels[next_offset + 3]; + let green_score = green_scores[next_pixel_index]; + let white_score = white_scores[next_pixel_index]; + let hint = background_hints[next_pixel_index]; + let reachable_soft_edge = hint > 0.08 + && alpha < 224 + && (green_score > 0.04 || white_score > 0.08 || alpha < 180); + let green_background = green_score >= GENERATED_ASSET_SHEET_HARD_GREEN_SCREEN_SCORE + || (alpha < 224 && green_score > GENERATED_ASSET_SHEET_GREEN_SCREEN_MIN_SCORE); + if alpha < 40 || green_background || white_score > 0.32 || reachable_soft_edge { + background_mask[next_pixel_index] = 1; + queue.push(next_pixel_index); + } + } + } + + for pixel_index in 0..pixel_count { + if background_mask[pixel_index] == 0 + && green_scores[pixel_index] >= GENERATED_ASSET_SHEET_HARD_GREEN_SCREEN_SCORE + { + background_mask[pixel_index] = 1; + } + } + + let soft_green_cleanup_rounds = (width.min(height) / 40).clamp(4, 14); + for _ in 0..soft_green_cleanup_rounds { + let mut expanded_mask = background_mask.clone(); + let mut changed_this_round = false; + for y in 0..height { + for x in 0..width { + let pixel_index = y * width + x; + if background_mask[pixel_index] != 0 { + continue; + } + let offset = pixel_index * 4; + let pixel = [ + pixels[offset], + pixels[offset + 1], + pixels[offset + 2], + pixels[offset + 3], + ]; + let green_score = green_scores[pixel_index]; + let white_score = white_scores[pixel_index]; + if !is_generated_asset_sheet_soft_green_matte_pixel(pixel, green_score, white_score) + { + continue; + } + if !touches_generated_asset_sheet_background_mask( + x, + y, + width, + height, + &background_mask, + ) { + continue; + } + + expanded_mask[pixel_index] = 1; + changed_this_round = true; + } + } + background_mask = expanded_mask; + if !changed_this_round { + break; + } + } + + for _ in 0..2 { + let mut expanded_mask = background_mask.clone(); + for y in 0..height { + for x in 0..width { + let pixel_index = y * width + x; + if background_mask[pixel_index] != 0 { + continue; + } + let alpha = pixels[pixel_index * 4 + 3]; + let green_score = green_scores[pixel_index]; + let white_score = white_scores[pixel_index]; + let hint = background_hints[pixel_index]; + let soft_matte_candidate = alpha < 224 + || white_score > 0.10 + || green_score >= GENERATED_ASSET_SHEET_HARD_GREEN_SCREEN_SCORE; + if hint < GENERATED_ASSET_SHEET_GREEN_SCREEN_SOFT_SCORE || !soft_matte_candidate { + continue; + } + + let mut adjacent_background_count = 0usize; + for offset_y in -1i32..=1 { + for offset_x in -1i32..=1 { + if offset_x == 0 && offset_y == 0 { + continue; + } + let next_x = x as i32 + offset_x; + let next_y = y as i32 + offset_y; + if next_x < 0 + || next_x >= width as i32 + || next_y < 0 + || next_y >= height as i32 + { + adjacent_background_count += 1; + continue; + } + if background_mask[next_y as usize * width + next_x as usize] != 0 { + adjacent_background_count += 1; + } + } + } + + if adjacent_background_count >= 2 + || (adjacent_background_count >= 1 + && hint >= GENERATED_ASSET_SHEET_GREEN_SCREEN_MIN_SCORE) + { + expanded_mask[pixel_index] = 1; + } + } + } + background_mask = expanded_mask; + } + + let mut changed = false; + for pixel_index in 0..pixel_count { + if background_mask[pixel_index] == 0 { + continue; + } + let alpha_offset = pixel_index * 4 + 3; + if pixels[alpha_offset] != 0 { + pixels[alpha_offset] = 0; + changed = true; + } + } + + for y in 0..height { + for x in 0..width { + let pixel_index = y * width + x; + let offset = pixel_index * 4; + let alpha = pixels[offset + 3]; + if alpha == 0 { + continue; + } + + let mut touches_transparent_edge = false; + for offset_y in -1i32..=1 { + for offset_x in -1i32..=1 { + if offset_x == 0 && offset_y == 0 { + continue; + } + let next_x = x as i32 + offset_x; + let next_y = y as i32 + offset_y; + if next_x < 0 || next_x >= width as i32 || next_y < 0 || next_y >= height as i32 + { + touches_transparent_edge = true; + continue; + } + let next_pixel_index = next_y as usize * width + next_x as usize; + if background_mask[next_pixel_index] != 0 + || pixels[next_pixel_index * 4 + 3] < 16 + { + touches_transparent_edge = true; + } + } + } + + if !touches_transparent_edge { + continue; + } + + let green_score = green_scores[pixel_index]; + let white_score = white_scores[pixel_index]; + let contamination = green_score.max(white_score).max(if alpha < 220 { + ((220 - alpha) as f32 / 220.0) * 0.25 + } else { + 0.0 + }); + if contamination < 0.06 { + continue; + } + + let sample = collect_generated_asset_sheet_foreground_neighbor_color( + pixels, + width, + height, + x, + y, + &background_mask, + &background_hints, + ); + let mut red = pixels[offset] as f32; + let mut green = pixels[offset + 1] as f32; + let mut blue = pixels[offset + 2] as f32; + let blend = clamp_generated_asset_sheet_unit(contamination.max(0.22)); + + if let Some((sample_red, sample_green, sample_blue)) = sample { + red = lerp_generated_asset_sheet_channel(red, sample_red as f32, blend); + green = lerp_generated_asset_sheet_channel(green, sample_green as f32, blend); + blue = lerp_generated_asset_sheet_channel(blue, sample_blue as f32, blend); + + if green_score > 0.04 { + green = green.min(sample_green as f32 + 18.0); + } + if white_score > 0.1 { + red = red.min(sample_red as f32 + 26.0); + green = green.min(sample_green as f32 + 26.0); + blue = blue.min(sample_blue as f32 + 26.0); + } + } else { + if green_score > 0.04 { + let toned_green = (green - (green - red.max(blue)) * 0.78) + .round() + .max(red.max(blue)); + green = green.min(toned_green).min(red.max(blue) + 18.0); + } + + if white_score > 0.12 { + let spread = red.max(green).max(blue) - red.min(green).min(blue); + if spread < 20.0 { + let toned_value = ((red + green + blue) / 3.0 * 0.88).round(); + red = red.min(toned_value); + green = green.min(toned_value); + blue = blue.min(toned_value); + } + } + } + + let mut next_alpha = alpha; + let edge_fade = (green_score * 0.35).max(white_score * 0.28); + if edge_fade > 0.08 { + next_alpha = ((alpha as f32) * (1.0 - edge_fade)).round() as u8; + if next_alpha < 10 { + next_alpha = 0; + } + } + + let next_red = red.round().clamp(0.0, 255.0) as u8; + let next_green = green.round().clamp(0.0, 255.0) as u8; + let next_blue = blue.round().clamp(0.0, 255.0) as u8; + if next_red != pixels[offset] + || next_green != pixels[offset + 1] + || next_blue != pixels[offset + 2] + || next_alpha != alpha + { + pixels[offset] = next_red; + pixels[offset + 1] = next_green; + pixels[offset + 2] = next_blue; + pixels[offset + 3] = next_alpha; + changed = true; + } + } + } + + changed +} + +fn touches_generated_asset_sheet_background_mask( + x: usize, + y: usize, + width: usize, + height: usize, + background_mask: &[u8], +) -> bool { + for offset_y in -1i32..=1 { + for offset_x in -1i32..=1 { + if offset_x == 0 && offset_y == 0 { + continue; + } + let next_x = x as i32 + offset_x; + let next_y = y as i32 + offset_y; + if next_x < 0 || next_x >= width as i32 || next_y < 0 || next_y >= height as i32 { + return true; + } + if background_mask[next_y as usize * width + next_x as usize] != 0 { + return true; + } + } + } + false +} + +fn is_generated_asset_sheet_soft_green_matte_pixel( + pixel: [u8; 4], + green_score: f32, + white_score: f32, +) -> bool { + if pixel[3] == 0 || green_score < GENERATED_ASSET_SHEET_GREEN_SCREEN_MIN_SCORE { + return false; + } + + let red = pixel[0]; + let green = pixel[1]; + let blue = pixel[2]; + let foreground_mix = red.max(blue); + green >= 188 + && white_score < 0.34 + && green.saturating_sub(foreground_mix) >= 42 + && (red >= 48 || blue >= 96 || pixel[3] < 236) +} + +fn compute_generated_asset_sheet_green_screen_score(pixel: [u8; 4]) -> f32 { + if pixel[3] == 0 { + return 1.0; + } + + let red = pixel[0] as f32; + let green = pixel[1] as f32; + let blue = pixel[2] as f32; + let green_lead = green - red.max(blue); + if green < 96.0 || green_lead <= 18.0 { + return 0.0; + } + + let green_ratio = green / (red + blue).max(1.0); + if green_ratio <= 0.9 { + return 0.0; + } + + (((green - 96.0) / 128.0).clamp(0.0, 1.0) * 0.34 + + ((green_lead - 18.0) / 120.0).clamp(0.0, 1.0) * 0.46 + + ((green_ratio - 0.9) / 2.4).clamp(0.0, 1.0) * 0.20) + .clamp(0.0, 1.0) +} + +fn compute_generated_asset_sheet_white_screen_score(pixel: [u8; 4]) -> f32 { + if pixel[3] == 0 { + return 1.0; + } + + let red = pixel[0] as f32; + let green = pixel[1] as f32; + let blue = pixel[2] as f32; + let max_channel = red.max(green).max(blue); + let min_channel = red.min(green).min(blue); + let average = (red + green + blue) / 3.0; + if average < 188.0 || min_channel < 168.0 { + return 0.0; + } + + let spread = max_channel - min_channel; + let neutrality = 1.0 - clamp_generated_asset_sheet_unit((spread - 6.0) / 34.0); + let brightness = clamp_generated_asset_sheet_unit((average - 188.0) / 55.0); + let floor = clamp_generated_asset_sheet_unit((min_channel - 168.0) / 60.0); + clamp_generated_asset_sheet_unit(neutrality * (brightness * 0.85 + floor * 0.15)) +} + +fn collect_generated_asset_sheet_foreground_neighbor_color( + pixels: &[u8], + width: usize, + height: usize, + x: usize, + y: usize, + background_mask: &[u8], + background_hints: &[f32], +) -> Option<(u8, u8, u8)> { + let mut total_weight = 0.0f32; + let mut total_red = 0.0f32; + let mut total_green = 0.0f32; + let mut total_blue = 0.0f32; + + for offset_y in -2i32..=2 { + for offset_x in -2i32..=2 { + if offset_x == 0 && offset_y == 0 { + continue; + } + let next_x = x as i32 + offset_x; + let next_y = y as i32 + offset_y; + if next_x < 0 || next_x >= width as i32 || next_y < 0 || next_y >= height as i32 { + continue; + } + + let next_pixel_index = next_y as usize * width + next_x as usize; + if background_mask[next_pixel_index] != 0 || background_hints[next_pixel_index] >= 0.18 + { + continue; + } + + let next_offset = next_pixel_index * 4; + let next_alpha = pixels[next_offset + 3]; + if next_alpha < 96 { + continue; + } + let distance = offset_x.unsigned_abs() + offset_y.unsigned_abs(); + let weight = (next_alpha as f32 / 255.0) + * if distance <= 1 { + 1.8 + } else if distance == 2 { + 1.2 + } else { + 0.7 + }; + + total_weight += weight; + total_red += pixels[next_offset] as f32 * weight; + total_green += pixels[next_offset + 1] as f32 * weight; + total_blue += pixels[next_offset + 2] as f32 * weight; + } + } + + if total_weight <= 0.0 { + return None; + } + + Some(( + (total_red / total_weight).round() as u8, + (total_green / total_weight).round() as u8, + (total_blue / total_weight).round() as u8, + )) +} + +fn sanitize_generated_asset_sheet_path_segment(raw: &str, fallback: &str) -> String { + let normalized = raw + .trim() + .chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' { + ch.to_ascii_lowercase() + } else { + '-' + } + }) + .collect::(); + let collapsed = normalized + .split('-') + .filter(|part| !part.is_empty()) + .collect::>() + .join("-"); + if collapsed.is_empty() { + fallback.to_string() + } else { + collapsed.chars().take(64).collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn build_test_image(width: u32, height: u32, color: [u8; 4]) -> image::RgbaImage { + image::RgbaImage::from_pixel(width, height, image::Rgba(color)) + } + + #[test] + fn generated_asset_sheet_prompt_uses_default_rows_and_special_instruction() { + let item_names = vec!["草莓".to_string(), "苹果".to_string()]; + + let prompt = build_generated_asset_sheet_prompt(&GeneratedAssetSheetPromptInput { + subject_text: "水果题材的抓大鹅 2D 物品素材", + item_names: &item_names, + grid_size: 5, + item_name_prompt_template: None, + special_prompt: None, + }) + .expect("prompt should build"); + + assert!(prompt.contains("5行*5列")); + assert!(prompt.contains("第1行:草莓 的 5 个不同视图")); + assert!(prompt.contains("第2行:苹果 的 5 个不同视图")); + assert!(prompt.contains("每个物品生成 5 个不同视图")); + } + + #[test] + fn generated_asset_sheet_prompt_allows_custom_row_template_and_special_prompt() { + let item_names = vec!["草莓".to_string()]; + + let prompt = build_generated_asset_sheet_prompt(&GeneratedAssetSheetPromptInput { + subject_text: "水果题材的抓大鹅 2D 物品素材", + item_names: &item_names, + grid_size: 5, + item_name_prompt_template: Some( + "第{row_index}行是 {item_name},共 {view_count} 个视图", + ), + special_prompt: Some("每个物品要生成五个不同视图:正面、左前、右前、俯视、背面。"), + }) + .expect("prompt should build"); + + assert!(prompt.contains("第1行是 草莓,共 5 个视图")); + assert!(prompt.contains("每个物品要生成五个不同视图")); + } + + #[test] + fn generated_asset_sheet_prompt_rejects_zero_grid_size() { + let item_names = vec!["草莓".to_string()]; + + let error = build_generated_asset_sheet_prompt(&GeneratedAssetSheetPromptInput { + subject_text: "水果题材的抓大鹅 2D 物品素材", + item_names: &item_names, + grid_size: 0, + item_name_prompt_template: None, + special_prompt: None, + }) + .expect_err("grid size 0 should be rejected"); + + assert_eq!(error.status_code(), StatusCode::BAD_REQUEST); + } + + #[test] + fn generated_asset_sheet_slices_by_requested_grid_size() { + let width = 500; + let height = 500; + let item_names = vec!["樱桃".to_string(), "苹果".to_string()]; + let mut sheet = image::RgbaImage::new(width, height); + for row in 0..5 { + for col in 0..5 { + let color = image::Rgba([ + 32 + row as u8 * 40, + 24 + col as u8 * 36, + 210 - row as u8 * 30, + 255, + ]); + for y in row * 100..(row + 1) * 100 { + for x in col * 100..(col + 1) * 100 { + sheet.put_pixel(x, y, color); + } + } + } + } + let mut encoded = std::io::Cursor::new(Vec::new()); + image::DynamicImage::ImageRgba8(sheet) + .write_to(&mut encoded, ImageFormat::Png) + .expect("sheet should encode"); + let image = DownloadedOpenAiImage { + bytes: encoded.into_inner(), + mime_type: "image/png".to_string(), + extension: "png".to_string(), + }; + + let slices = + slice_generated_asset_sheet(&image, &item_names, 5).expect("sheet should slice"); + + assert_eq!(slices.len(), 2); + assert_eq!(slices[0].len(), 5); + assert_eq!(slices[1].len(), 5); + } + + #[test] + fn generated_asset_sheet_prepare_put_request_packs_prompt_metadata() { + let request = prepare_generated_asset_sheet_put_request(GeneratedAssetSheetPersistInput { + prefix: LegacyAssetPrefix::Match3DAssets, + owner_user_id: "user-1".to_string(), + session_id: "session-1".to_string(), + profile_id: "profile-1".to_string(), + path_segments: vec!["items".to_string(), "view".to_string()], + file_name: "view-01.png".to_string(), + content_type: "image/png".to_string(), + bytes: b"sheet-bytes".to_vec(), + asset_kind: "match3d_item_image_view".to_string(), + source_job_id: Some("task-1".to_string()), + generated_at_micros: 123, + grid_size: 5, + row_index: 1, + view_index: 2, + prompt: GeneratedAssetSheetPersistPrompt { + sheet_prompt: Some("sheet prompt".to_string()), + item_name_prompt: Some("item prompt".to_string()), + special_prompt: Some("special prompt".to_string()), + }, + }) + .expect("request should prepare"); + + assert_eq!( + request + .metadata + .get("x-oss-meta-generated-asset-sheet-prompt-encoding"), + Some(&"utf8-base64".to_string()) + ); + assert_eq!( + request + .metadata + .get("x-oss-meta-generated-asset-sheet-grid-size"), + Some(&"5".to_string()) + ); + assert_eq!( + request + .metadata + .get("x-oss-meta-generated-asset-sheet-row-index"), + Some(&"1".to_string()) + ); + assert_eq!( + request + .metadata + .get("x-oss-meta-generated-asset-sheet-view-index"), + Some(&"2".to_string()) + ); + assert_eq!( + request + .metadata + .get("x-oss-meta-generated-asset-sheet-prompt-b64"), + Some(&BASE64_STANDARD.encode("sheet prompt")) + ); + assert_eq!( + request + .metadata + .get("x-oss-meta-generated-asset-sheet-item-name-prompt-b64"), + Some(&BASE64_STANDARD.encode("item prompt")) + ); + assert_eq!( + request + .metadata + .get("x-oss-meta-generated-asset-sheet-special-prompt-b64"), + Some(&BASE64_STANDARD.encode("special prompt")) + ); + } +} diff --git a/server-rs/crates/api-server/src/jump_hop.rs b/server-rs/crates/api-server/src/jump_hop.rs new file mode 100644 index 00000000..ec6d0a43 --- /dev/null +++ b/server-rs/crates/api-server/src/jump_hop.rs @@ -0,0 +1,447 @@ +use axum::{ + Json, + extract::{Extension, Path, State, rejection::JsonRejection}, + http::{HeaderName, StatusCode, header}, + response::Response, +}; +use serde_json::{Value, json}; +use shared_contracts::jump_hop::{ + JumpHopActionRequest, JumpHopDraftResponse, JumpHopGalleryDetailResponse, + JumpHopGenerationStatus, JumpHopJumpRequest, JumpHopJumpResponse, JumpHopRestartRunRequest, + JumpHopRunResponse, JumpHopSessionResponse, JumpHopSessionSnapshotResponse, + JumpHopStartRunRequest, JumpHopWorkDetailResponse, JumpHopWorkMutationResponse, + JumpHopWorkspaceCreateRequest, +}; +use shared_kernel::{build_prefixed_uuid_id, format_timestamp_micros}; +use spacetime_client::SpacetimeClientError; +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::{ + api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError, + request_context::RequestContext, state::AppState, +}; + +const JUMP_HOP_PROVIDER: &str = "jump-hop"; +const JUMP_HOP_CREATION_PROVIDER: &str = "jump-hop-creation"; +const JUMP_HOP_RUNTIME_PROVIDER: &str = "jump-hop-runtime"; +const JUMP_HOP_TEMPLATE_ID: &str = "jump-hop"; +const JUMP_HOP_TEMPLATE_NAME: &str = "跳一跳"; + +pub async fn create_jump_hop_session( + State(state): State, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let Json(payload) = jump_hop_json(payload, &request_context, JUMP_HOP_CREATION_PROVIDER)?; + validate_workspace_request(&request_context, &payload)?; + + let owner_user_id = authenticated.claims().user_id().to_string(); + let session_id = build_prefixed_uuid_id("jump-hop-session-"); + let now = current_utc_micros(); + let draft = build_jump_hop_draft(&payload); + let session = JumpHopSessionSnapshotResponse { + session_id, + owner_user_id, + status: JumpHopGenerationStatus::Draft, + draft: Some(draft), + created_at: format_timestamp_micros(now), + updated_at: format_timestamp_micros(now), + }; + + Ok(json_success_body( + Some(&request_context), + JumpHopSessionResponse { + session: state + .spacetime_client() + .create_jump_hop_session(session) + .await + .map_err(|error| { + jump_hop_error_response( + &request_context, + JUMP_HOP_CREATION_PROVIDER, + map_jump_hop_client_error(error), + ) + })?, + }, + )) +} + +pub async fn get_jump_hop_session( + State(state): State, + Path(session_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, +) -> Result, Response> { + ensure_non_empty(&request_context, &session_id, "sessionId")?; + let owner_user_id = authenticated.claims().user_id().to_string(); + let session = state + .spacetime_client() + .get_jump_hop_session(session_id, owner_user_id) + .await + .map_err(|error| { + jump_hop_error_response( + &request_context, + JUMP_HOP_CREATION_PROVIDER, + map_jump_hop_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + JumpHopSessionResponse { session }, + )) +} + +pub async fn execute_jump_hop_action( + State(state): State, + Path(session_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + ensure_non_empty(&request_context, &session_id, "sessionId")?; + let Json(payload) = jump_hop_json(payload, &request_context, JUMP_HOP_CREATION_PROVIDER)?; + let owner_user_id = authenticated.claims().user_id().to_string(); + let response = state + .spacetime_client() + .execute_jump_hop_action(session_id, owner_user_id, payload) + .await + .map_err(|error| { + jump_hop_error_response( + &request_context, + JUMP_HOP_CREATION_PROVIDER, + map_jump_hop_client_error(error), + ) + })?; + + Ok(json_success_body(Some(&request_context), response)) +} + +pub async fn publish_jump_hop_work( + State(state): State, + Path(profile_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, +) -> Result, Response> { + ensure_non_empty(&request_context, &profile_id, "profileId")?; + let work = state + .spacetime_client() + .publish_jump_hop_work(profile_id, authenticated.claims().user_id().to_string()) + .await + .map_err(|error| { + jump_hop_error_response( + &request_context, + JUMP_HOP_CREATION_PROVIDER, + map_jump_hop_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + JumpHopWorkMutationResponse { item: work }, + )) +} + +pub async fn get_jump_hop_runtime_work( + State(state): State, + Path(profile_id): Path, + Extension(request_context): Extension, +) -> Result, Response> { + ensure_non_empty(&request_context, &profile_id, "profileId")?; + let work = state + .spacetime_client() + .get_jump_hop_runtime_work(profile_id) + .await + .map_err(|error| { + jump_hop_error_response( + &request_context, + JUMP_HOP_RUNTIME_PROVIDER, + map_jump_hop_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + JumpHopWorkDetailResponse { item: work }, + )) +} + +pub async fn start_jump_hop_run( + State(state): State, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let Json(payload) = jump_hop_json(payload, &request_context, JUMP_HOP_RUNTIME_PROVIDER)?; + ensure_non_empty(&request_context, &payload.profile_id, "profileId")?; + let run = state + .spacetime_client() + .start_jump_hop_run(payload, authenticated.claims().user_id().to_string()) + .await + .map_err(|error| { + jump_hop_error_response( + &request_context, + JUMP_HOP_RUNTIME_PROVIDER, + map_jump_hop_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + JumpHopRunResponse { run }, + )) +} + +pub async fn jump_hop_run_jump( + State(state): State, + Path(run_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + ensure_non_empty(&request_context, &run_id, "runId")?; + let Json(payload) = jump_hop_json(payload, &request_context, JUMP_HOP_RUNTIME_PROVIDER)?; + let run = state + .spacetime_client() + .jump_hop_run_jump( + run_id, + authenticated.claims().user_id().to_string(), + payload, + ) + .await + .map_err(|error| { + jump_hop_error_response( + &request_context, + JUMP_HOP_RUNTIME_PROVIDER, + map_jump_hop_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + JumpHopJumpResponse { run }, + )) +} + +pub async fn restart_jump_hop_run( + State(state): State, + Path(run_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + ensure_non_empty(&request_context, &run_id, "runId")?; + let Json(payload) = jump_hop_json(payload, &request_context, JUMP_HOP_RUNTIME_PROVIDER)?; + let run = state + .spacetime_client() + .restart_jump_hop_run( + run_id, + authenticated.claims().user_id().to_string(), + payload, + ) + .await + .map_err(|error| { + jump_hop_error_response( + &request_context, + JUMP_HOP_RUNTIME_PROVIDER, + map_jump_hop_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + JumpHopRunResponse { run }, + )) +} + +pub async fn list_jump_hop_gallery( + State(state): State, + Extension(request_context): Extension, +) -> Result, Response> { + let gallery = state + .spacetime_client() + .list_jump_hop_gallery() + .await + .map_err(|error| { + jump_hop_error_response( + &request_context, + JUMP_HOP_RUNTIME_PROVIDER, + map_jump_hop_client_error(error), + ) + })?; + + Ok(json_success_body(Some(&request_context), gallery)) +} + +pub async fn get_jump_hop_gallery_detail( + State(state): State, + Path(public_work_code): Path, + Extension(request_context): Extension, +) -> Result, Response> { + ensure_non_empty(&request_context, &public_work_code, "publicWorkCode")?; + let work = state + .spacetime_client() + .get_jump_hop_gallery_detail(public_work_code) + .await + .map_err(|error| { + jump_hop_error_response( + &request_context, + JUMP_HOP_RUNTIME_PROVIDER, + map_jump_hop_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + JumpHopGalleryDetailResponse { item: work }, + )) +} + +fn build_jump_hop_draft(payload: &JumpHopWorkspaceCreateRequest) -> JumpHopDraftResponse { + JumpHopDraftResponse { + template_id: JUMP_HOP_TEMPLATE_ID.to_string(), + template_name: JUMP_HOP_TEMPLATE_NAME.to_string(), + profile_id: None, + work_title: payload.work_title.trim().to_string(), + work_description: payload.work_description.trim().to_string(), + theme_tags: normalize_tags(payload.theme_tags.clone()), + difficulty: payload.difficulty.clone(), + style_preset: payload.style_preset.clone(), + character_prompt: payload.character_prompt.trim().to_string(), + tile_prompt: payload.tile_prompt.trim().to_string(), + end_mood_prompt: payload + .end_mood_prompt + .as_ref() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()), + character_asset: None, + tile_atlas_asset: None, + tile_assets: Vec::new(), + path: None, + cover_composite: None, + generation_status: JumpHopGenerationStatus::Draft, + } +} + +fn validate_workspace_request( + request_context: &RequestContext, + payload: &JumpHopWorkspaceCreateRequest, +) -> Result<(), Response> { + ensure_non_empty(request_context, &payload.work_title, "workTitle")?; + ensure_non_empty( + request_context, + &payload.character_prompt, + "characterPrompt", + )?; + ensure_non_empty(request_context, &payload.tile_prompt, "tilePrompt")?; + if payload.template_id.trim() != JUMP_HOP_TEMPLATE_ID { + return Err(jump_hop_error_response( + request_context, + JUMP_HOP_CREATION_PROVIDER, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": JUMP_HOP_PROVIDER, + "message": "templateId 必须为 jump-hop", + })), + )); + } + Ok(()) +} + +fn ensure_non_empty( + request_context: &RequestContext, + value: &str, + field: &str, +) -> Result<(), Response> { + if value.trim().is_empty() { + return Err(jump_hop_error_response( + request_context, + JUMP_HOP_PROVIDER, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": JUMP_HOP_PROVIDER, + "field": field, + "message": format!("{field} 不能为空"), + })), + )); + } + Ok(()) +} + +fn normalize_tags(tags: Vec) -> Vec { + let mut normalized = Vec::new(); + for tag in tags { + let tag = tag.trim(); + if tag.is_empty() || normalized.iter().any(|item| item == tag) { + continue; + } + normalized.push(tag.to_string()); + if normalized.len() >= 6 { + break; + } + } + normalized +} + +fn jump_hop_json( + payload: Result, JsonRejection>, + request_context: &RequestContext, + provider: &str, +) -> Result, Response> { + payload.map_err(|error| { + jump_hop_error_response( + request_context, + provider, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": provider, + "message": error.to_string(), + })), + ) + }) +} + +fn map_jump_hop_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("必须") => + { + StatusCode::BAD_REQUEST + } + _ => StatusCode::BAD_GATEWAY, + }; + + AppError::from_status(status).with_details(json!({ + "provider": "spacetimedb", + "message": error.to_string(), + })) +} + +fn jump_hop_error_response( + request_context: &RequestContext, + provider: &str, + 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_str(provider) + .unwrap_or_else(|_| header::HeaderValue::from_static("jump-hop")), + ); + response +} + +fn current_utc_micros() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_micros().min(i64::MAX as u128) as i64) + .unwrap_or(0) +} diff --git a/server-rs/crates/api-server/src/main.rs b/server-rs/crates/api-server/src/main.rs index 01ed6555..0a58d126 100644 --- a/server-rs/crates/api-server/src/main.rs +++ b/server-rs/crates/api-server/src/main.rs @@ -39,10 +39,13 @@ mod custom_world_rpg_draft_prompts; mod edutainment_baby_drawing; mod edutainment_baby_object; mod error_middleware; +mod external_api_audit; +pub(crate) mod generated_asset_sheets; mod generated_image_assets; mod health; mod http_error; mod hyper3d_generation; +mod jump_hop; mod llm; mod llm_model_routing; mod login_options; diff --git a/server-rs/crates/api-server/src/match3d.rs b/server-rs/crates/api-server/src/match3d.rs index 405393cd..62d73cf7 100644 --- a/server-rs/crates/api-server/src/match3d.rs +++ b/server-rs/crates/api-server/src/match3d.rs @@ -16,7 +16,7 @@ use axum::{ }; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; use futures_util::{StreamExt, stream::FuturesUnordered}; -use image::{GenericImageView, ImageFormat}; +use image::ImageFormat; use module_match3d::{ MATCH3D_MESSAGE_ID_PREFIX, MATCH3D_PROFILE_ID_PREFIX, MATCH3D_RUN_ID_PREFIX, MATCH3D_SESSION_ID_PREFIX, @@ -71,10 +71,12 @@ use crate::{ }, auth::AuthenticatedAccessToken, config::AppConfig, + generated_asset_sheets::apply_generated_asset_sheet_green_screen_alpha, http_error::AppError, openai_image_generation::{ DownloadedOpenAiImage, OpenAiGeneratedImages, OpenAiReferenceImage, - build_openai_image_http_client, create_openai_image_edit, create_openai_image_generation, + build_openai_image_http_client, create_openai_image_edit, + create_openai_image_edit_with_references, create_openai_image_generation, require_openai_image_settings, }, platform_errors::map_oss_error, @@ -95,15 +97,10 @@ const MATCH3D_DEFAULT_DIFFICULTY: u32 = 4; const MATCH3D_DRAFT_GENERATION_POINTS_COST: u64 = 10; const MATCH3D_BACKGROUND_IMAGE_POINTS_COST: u64 = 2; const MATCH3D_ITEM_ASSETS_POINTS_PER_BATCH: u64 = 2; -const MATCH3D_MATERIAL_ITEM_BATCH_SIZE: usize = 5; +const MATCH3D_MATERIAL_ITEM_BATCH_SIZE: usize = 20; const MATCH3D_ITEM_VIEW_COUNT: usize = 5; -const MATCH3D_MATERIAL_GRID_SIZE: u32 = 5; -const MATCH3D_MATERIAL_FOREGROUND_DIFF_THRESHOLD: i32 = 36; -const MATCH3D_MATERIAL_FOREGROUND_ALPHA_THRESHOLD: i32 = 36; -const MATCH3D_MATERIAL_GREEN_SCREEN_MIN_SCORE: f32 = 0.34; -const MATCH3D_MATERIAL_GREEN_SCREEN_SOFT_SCORE: f32 = 0.18; -const MATCH3D_MATERIAL_HARD_GREEN_SCREEN_SCORE: f32 = 0.82; -const MATCH3D_MAX_GENERATED_ITEM_COUNT: usize = 25; +const MATCH3D_MATERIAL_GRID_SIZE: u32 = 10; +const MATCH3D_MAX_GENERATED_ITEM_COUNT: usize = 20; const MATCH3D_MATERIAL_VECTOR_ENGINE_GEMINI_MODEL: &str = "gemini-3-pro-image-preview"; const MATCH3D_MATERIAL_VECTOR_ENGINE_GEMINI_ASPECT_RATIO: &str = "1:1"; const MATCH3D_LEGACY_MODEL_DOWNLOAD_TIMEOUT_MS: u64 = 3 * 60_000; @@ -114,15 +111,16 @@ const MATCH3D_WORK_METADATA_LLM_MODEL: &str = "gpt-4o"; const MATCH3D_ITEM_SIZE_LARGE: &str = "大"; const MATCH3D_ITEM_SIZE_MEDIUM: &str = "中"; const MATCH3D_ITEM_SIZE_SMALL: &str = "小"; -const MATCH3D_CONTAINER_REFERENCE_IMAGE_PATH: &str = - "public/match3d-background-references/pot-fused-reference.png"; +const MATCH3D_CONTAINER_REFERENCE_IMAGE_BYTES: &[u8] = + include_bytes!("../../../../public/match3d-background-references/pot-fused-reference.png"); +const MATCH3D_PUBLIC_REFERENCE_IMAGE_PATH_PREFIX: &str = "public/"; const MATCH3D_QUESTION_THEME: &str = "你想创作什么题材"; const MATCH3D_QUESTION_CLEAR_COUNT: &str = "需要消除多少次才能通关"; const MATCH3D_QUESTION_DIFFICULTY: &str = "如果难度是从1-10,你要创作的关卡是难度几"; const MATCH3D_CLICK_SOUND_ASSET_KIND: &str = "match3d_click_sound"; const MATCH3D_PIXEL_RETRO_STYLE_PROMPT: &str = "真正复古像素 2D 游戏道具 sprite 风格,先以约 64x64 低分辨率像素块绘制再按整数倍放大,硬边方块像素清晰可见,有限色板 12-24 色,禁止抗锯齿、柔焦、平滑渐变、真实 3D 渲染、PBR 材质和摄影光照。"; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct Match3DConfigJson { theme_text: String, @@ -174,15 +172,33 @@ struct Match3DGeneratedItemImageView { image_object_key: Option, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct Match3DGeneratedBackgroundAsset { prompt: String, #[serde(default)] + level_scene_prompt: Option, + #[serde(default)] + level_scene_image_src: Option, + #[serde(default)] + level_scene_image_object_key: Option, + #[serde(default)] image_src: Option, #[serde(default)] image_object_key: Option, #[serde(default)] + ui_spritesheet_prompt: Option, + #[serde(default)] + ui_spritesheet_image_src: Option, + #[serde(default)] + ui_spritesheet_image_object_key: Option, + #[serde(default)] + item_spritesheet_prompt: Option, + #[serde(default)] + item_spritesheet_image_src: Option, + #[serde(default)] + item_spritesheet_image_object_key: Option, + #[serde(default)] container_prompt: Option, #[serde(default)] container_image_src: Option, @@ -449,8 +465,17 @@ impl From .background_asset .map(|asset| Match3DGeneratedBackgroundAsset { prompt: asset.prompt, + level_scene_prompt: asset.level_scene_prompt, + level_scene_image_src: asset.level_scene_image_src, + level_scene_image_object_key: asset.level_scene_image_object_key, image_src: asset.image_src, image_object_key: asset.image_object_key, + ui_spritesheet_prompt: asset.ui_spritesheet_prompt, + ui_spritesheet_image_src: asset.ui_spritesheet_image_src, + ui_spritesheet_image_object_key: asset.ui_spritesheet_image_object_key, + item_spritesheet_prompt: asset.item_spritesheet_prompt, + item_spritesheet_image_src: asset.item_spritesheet_image_src, + item_spritesheet_image_object_key: asset.item_spritesheet_image_object_key, container_prompt: asset.container_prompt, container_image_src: asset.container_image_src, container_image_object_key: asset.container_image_object_key, diff --git a/server-rs/crates/api-server/src/match3d/draft.rs b/server-rs/crates/api-server/src/match3d/draft.rs index 1f73f265..79a0aa77 100644 --- a/server-rs/crates/api-server/src/match3d/draft.rs +++ b/server-rs/crates/api-server/src/match3d/draft.rs @@ -229,12 +229,27 @@ pub(super) async fn compile_match3d_draft_for_session( ) .await?; - let existing_assets = get_match3d_existing_generated_item_assets( + let mut existing_assets = get_match3d_existing_generated_item_assets( state, owner_user_id.as_str(), profile_id.as_str(), ) .await; + let generated_background_asset = resolve_or_generate_match3d_level_asset_bundle( + state, + request_context, + owner_user_id.as_str(), + session.session_id.as_str(), + profile_id.as_str(), + &config, + generated_work_metadata.background_prompt.as_str(), + &existing_assets, + ) + .await?; + attach_match3d_background_asset_to_assets( + &mut existing_assets, + generated_background_asset.clone(), + ); let generated_item_assets = generate_match3d_item_assets( state, request_context, @@ -245,18 +260,22 @@ pub(super) async fn compile_match3d_draft_for_session( &config, generated_work_metadata.items, existing_assets, + Some(generated_background_asset.clone()), ) .await?; - let generated_item_assets = ensure_match3d_background_asset( + let mut generated_item_assets = generated_item_assets; + attach_match3d_background_asset_to_assets( + &mut generated_item_assets, + generated_background_asset, + ); + persist_match3d_generated_item_assets_snapshot( state, request_context, authenticated, - owner_user_id.as_str(), session.session_id.as_str(), + owner_user_id.as_str(), profile_id.as_str(), - &config, - generated_work_metadata.background_prompt.as_str(), - generated_item_assets, + &generated_item_assets, ) .await?; let existing_cover_image_src = get_match3d_existing_cover_image_src( diff --git a/server-rs/crates/api-server/src/match3d/item_assets.rs b/server-rs/crates/api-server/src/match3d/item_assets.rs index b5f6158c..1ecf326d 100644 --- a/server-rs/crates/api-server/src/match3d/item_assets.rs +++ b/server-rs/crates/api-server/src/match3d/item_assets.rs @@ -1,4 +1,11 @@ use super::*; +#[cfg(test)] +use crate::generated_asset_sheets::crop_generated_asset_sheet_view_edge_matte; +use crate::generated_asset_sheets::{ + GeneratedAssetSheetPersistInput, GeneratedAssetSheetPersistPrompt, + GeneratedAssetSheetSliceImage, persist_generated_asset_sheet_bytes, + slice_generated_asset_sheet_two_items_per_row, +}; pub(super) async fn generate_match3d_item_assets( state: &AppState, @@ -10,6 +17,7 @@ pub(super) async fn generate_match3d_item_assets( config: &Match3DConfigJson, item_plan: Vec, existing_assets: Vec, + generated_background_asset: Option, ) -> Result, Response> { // 中文注释:抓大鹅音频生成当前关闭;自动草稿只补齐 2D 物品图片和可选点击音效。 let target_item_count = resolve_match3d_generated_item_count(config); @@ -29,6 +37,7 @@ pub(super) async fn generate_match3d_item_assets( config, item_plan, assets, + generated_background_asset, ) .await?; } @@ -68,6 +77,7 @@ async fn ensure_match3d_item_image_assets( config: &Match3DConfigJson, item_plan: Vec, existing_assets: Vec, + generated_background_asset: Option, ) -> Result, Response> { let mut assets = normalize_match3d_generated_item_assets_for_resume(existing_assets); let target_item_count = resolve_match3d_generated_item_count(config); @@ -93,9 +103,11 @@ async fn ensure_match3d_item_image_assets( background_music_style: None, background_music_prompt: None, background_asset: if index == 0 { - assets - .first() - .and_then(|asset| asset.background_asset.clone()) + generated_background_asset.clone().or_else(|| { + assets + .first() + .and_then(|asset| asset.background_asset.clone()) + }) } else { None }, @@ -151,8 +163,14 @@ struct Match3DItemImageGenerationSeed { struct Match3DMaterialBatchOutput { task_id: String, + prompt: String, + image_src: Option, + image_object_key: Option, generated_at_micros: i64, - items: Vec<(Match3DItemImageGenerationSeed, Vec)>, + items: Vec<( + Match3DItemImageGenerationSeed, + Vec, + )>, } struct Match3DGeneratedItemImageAssetOutput { @@ -182,12 +200,17 @@ async fn generate_match3d_item_image_assets_in_batches( .map(|chunk| { let chunk_seeds = chunk.to_vec(); async move { - let item_names = chunk_seeds - .iter() - .map(|item| item.item_name.clone()) - .collect::>(); - let material_sheet = - generate_match3d_material_sheet(state, config, &item_names).await?; + let material_sheet = generate_match3d_material_sheet_from_level_scene( + state, + owner_user_id, + session_id, + profile_id, + config, + chunk_seeds + .iter() + .find_map(|seed| seed.background_asset.as_ref()), + ) + .await?; let generated_at_micros = current_utc_micros(); let persisted_seed_count = chunk_seeds .iter() @@ -206,10 +229,17 @@ async fn generate_match3d_item_image_assets_in_batches( .iter() .map(|item| item.item_name.clone()) .collect::>(); - let item_images = - slice_match3d_material_sheet(&material_sheet.image, &persisted_item_names)?; + let item_images = slice_generated_asset_sheet_two_items_per_row( + &material_sheet.image, + &persisted_item_names, + MATCH3D_MATERIAL_GRID_SIZE as usize, + MATCH3D_ITEM_VIEW_COUNT, + )?; Ok::<_, AppError>(Match3DMaterialBatchOutput { task_id: material_sheet.task_id, + prompt: material_sheet.prompt, + image_src: material_sheet.image_src, + image_object_key: material_sheet.image_object_key, generated_at_micros, items: persisted_seeds .into_iter() @@ -231,26 +261,52 @@ async fn generate_match3d_item_image_assets_in_batches( let mut generated_assets = Vec::new(); for batch in batches { let sheet_task_id = batch.task_id; + let sheet_prompt = batch.prompt; + let sheet_image_src = batch.image_src; + let sheet_image_object_key = batch.image_object_key; let generated_at_micros = batch.generated_at_micros; for (item_index, (seed, item_images)) in batch.items.into_iter().enumerate() { let item_slug = build_match3d_item_slug(seed.item_id.as_str(), seed.item_name.as_str()); let mut image_views = Vec::with_capacity(item_images.len()); for (view_index, item_image) in item_images.into_iter().enumerate() { let view_number = view_index + 1; - let view_upload = persist_match3d_generated_bytes( + let (sheet_row_index, sheet_col_index) = + resolve_match3d_material_sheet_cell_indices(item_index, view_index); + let item_name_prompt = format!( + "第{}行第{}种:{} 的 5 个不同形态", + item_index / 2 + 1, + item_index % 2 + 1, + seed.item_name + ); + let view_upload = persist_generated_asset_sheet_bytes( state, - owner_user_id, - session_id, - profile_id, - &["items", item_slug.as_str(), "views"], - format!("view-{view_number:02}.png").as_str(), - "image/png", - item_image.bytes, - "match3d_item_image_view", - Some(sheet_task_id.as_str()), - generated_at_micros.saturating_add( - (item_index * MATCH3D_ITEM_VIEW_COUNT + view_index) as i64 + 1, - ), + GeneratedAssetSheetPersistInput { + prefix: LegacyAssetPrefix::Match3DAssets, + owner_user_id: owner_user_id.to_string(), + session_id: session_id.to_string(), + profile_id: profile_id.to_string(), + path_segments: vec![ + "items".to_string(), + item_slug.clone(), + "views".to_string(), + ], + file_name: format!("view-{view_number:02}.png"), + content_type: "image/png".to_string(), + bytes: item_image.bytes, + asset_kind: "match3d_item_image_view".to_string(), + source_job_id: Some(sheet_task_id.clone()), + generated_at_micros: generated_at_micros.saturating_add( + (item_index * MATCH3D_ITEM_VIEW_COUNT + view_index) as i64 + 1, + ), + grid_size: MATCH3D_MATERIAL_GRID_SIZE as usize, + row_index: sheet_row_index, + view_index: sheet_col_index, + prompt: GeneratedAssetSheetPersistPrompt { + sheet_prompt: Some(sheet_prompt.clone()), + item_name_prompt: Some(item_name_prompt), + special_prompt: Some(match3d_material_sheet_special_prompt()), + }, + }, ) .await .map_err(|error| match3d_error_response(request_context, provider, error))?; @@ -288,7 +344,12 @@ async fn generate_match3d_item_image_assets_in_batches( background_music_prompt: seed.background_music_prompt, background_music: None, click_sound: None, - background_asset: seed.background_asset, + background_asset: merge_match3d_item_spritesheet_asset_metadata( + seed.background_asset, + sheet_prompt.clone(), + sheet_image_src.clone(), + sheet_image_object_key.clone(), + ), status: "image_ready".to_string(), error: None, }, @@ -478,6 +539,7 @@ async fn append_match3d_new_item_assets( return Ok(assets); } let mut next_item_index = next_match3d_generated_item_index(&assets); + let background_asset = find_match3d_generated_background_asset(&assets); let item_seeds = append_plan .padded_item_names .into_iter() @@ -493,7 +555,11 @@ async fn append_match3d_new_item_assets( background_music_title: None, background_music_style: None, background_music_prompt: None, - background_asset: None, + background_asset: if index == 0 { + background_asset.clone() + } else { + None + }, } }) .collect::>(); @@ -662,6 +728,9 @@ async fn replace_match3d_item_assets( pub(super) struct Match3DMaterialSheet { pub(super) task_id: String, + pub(super) prompt: String, + pub(super) image_src: Option, + pub(super) image_object_key: Option, pub(super) image: DownloadedOpenAiImage, } @@ -671,9 +740,122 @@ pub(super) struct Match3DVectorEngineGeminiImageSettings { pub(super) request_timeout_ms: u64, } +#[cfg(test)] pub(super) struct Match3DSlicedItemImage { pub(super) bytes: Vec, } + +async fn generate_match3d_material_sheet_from_level_scene( + state: &AppState, + owner_user_id: &str, + session_id: &str, + profile_id: &str, + config: &Match3DConfigJson, + background_asset: Option<&Match3DGeneratedBackgroundAsset>, +) -> Result { + let settings = require_openai_image_settings(state)?; + let http_client = build_openai_image_http_client(&settings)?; + let prompt = build_match3d_item_spritesheet_prompt(); + let reference = load_match3d_level_scene_reference_image(state, background_asset).await?; + let generated = create_openai_image_edit( + &http_client, + &settings, + prompt.as_str(), + Some(build_match3d_material_sheet_negative_prompt(config).as_str()), + "2k", + &reference, + "抓大鹅物品 spritesheet 生成失败", + ) + .await?; + let image = generated.images.into_iter().next().ok_or_else(|| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "vector-engine", + "message": "抓大鹅物品 spritesheet 生成失败:未返回图片", + })) + })?; + let image = make_match3d_spritesheet_image_transparent(image)?; + let upload = persist_match3d_generated_bytes( + state, + owner_user_id, + session_id, + profile_id, + &["item-spritesheet", generated.task_id.as_str()], + "item-spritesheet.png", + image.mime_type.as_str(), + image.bytes.clone(), + "match3d_item_spritesheet_image", + Some(generated.task_id.as_str()), + current_utc_micros(), + ) + .await?; + Ok(Match3DMaterialSheet { + task_id: generated.task_id, + prompt, + image_src: Some(upload.src), + image_object_key: Some(upload.object_key), + image, + }) +} + +fn merge_match3d_item_spritesheet_asset_metadata( + background_asset: Option, + prompt: String, + image_src: Option, + image_object_key: Option, +) -> Option { + background_asset.map(|mut asset| { + asset.item_spritesheet_prompt = Some(prompt); + asset.item_spritesheet_image_src = image_src; + asset.item_spritesheet_image_object_key = image_object_key; + asset + }) +} + +async fn load_match3d_level_scene_reference_image( + state: &AppState, + background_asset: Option<&Match3DGeneratedBackgroundAsset>, +) -> Result { + let Some(source) = background_asset + .and_then(|asset| { + asset + .level_scene_image_object_key + .as_deref() + .or(asset.level_scene_image_src.as_deref()) + }) + .map(str::trim) + .filter(|value| !value.is_empty()) + else { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": MATCH3D_AGENT_PROVIDER, + "message": "抓大鹅物品 spritesheet 生成缺少关卡画面参考图", + })), + ); + }; + let bytes = if source.starts_with("data:image/") { + decode_match3d_data_url_bytes(source)? + } else if source.trim_start_matches('/').starts_with("generated-") { + read_match3d_generated_object_bytes( + state, + source, + "读取抓大鹅关卡画面参考图失败", + MATCH3D_ITEM_IMAGE_MAX_BYTES, + ) + .await? + } else { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": MATCH3D_AGENT_PROVIDER, + "message": "抓大鹅关卡画面参考图必须是图片 Data URL 或 /generated-* 路径", + })), + ); + }; + Ok(OpenAiReferenceImage { + bytes, + mime_type: "image/png".to_string(), + file_name: "match3d-level-scene.png".to_string(), + }) +} pub(super) fn normalize_match3d_item_name(raw: &str) -> String { raw.trim() .trim_matches(['"', '\'', '“', '”', '。', ',', ',', '、']) @@ -1079,20 +1261,20 @@ pub(super) fn resolve_match3d_gameplay_item_count(config: &Match3DConfigJson) -> 8 => 3, 12 => 9, 16 => 15, - 20 | 21 => 21, + 20 | 21 => 20, _ => match config.difficulty { 0..=2 => 3, 3..=4 => 9, 5..=6 => 15, - _ => 21, + _ => 20, }, } .min(MATCH3D_MAX_GENERATED_ITEM_COUNT) } pub(super) fn resolve_match3d_generated_item_count(config: &Match3DConfigJson) -> usize { - round_match3d_item_count_to_full_sheet(resolve_match3d_gameplay_item_count(config)) - .min(MATCH3D_MAX_GENERATED_ITEM_COUNT) + let _ = config; + MATCH3D_MAX_GENERATED_ITEM_COUNT } fn round_match3d_item_count_to_full_sheet(item_count: usize) -> usize { @@ -1102,6 +1284,16 @@ fn round_match3d_item_count_to_full_sheet(item_count: usize) -> usize { item_count.div_ceil(MATCH3D_MATERIAL_ITEM_BATCH_SIZE) * MATCH3D_MATERIAL_ITEM_BATCH_SIZE } +pub(super) fn resolve_match3d_material_sheet_cell_indices( + item_index: usize, + view_index: usize, +) -> (usize, usize) { + let items_per_row = (MATCH3D_MATERIAL_GRID_SIZE as usize / MATCH3D_ITEM_VIEW_COUNT).max(1); + let row_index = item_index / items_per_row + 1; + let col_index = (item_index % items_per_row) * MATCH3D_ITEM_VIEW_COUNT + view_index + 1; + (row_index, col_index) +} + pub(super) fn sort_match3d_generated_assets( mut assets: Vec, ) -> Vec { @@ -1259,11 +1451,23 @@ pub(super) fn is_match3d_background_asset_ready(asset: &Match3DGeneratedBackgrou .filter(|value| !value.is_empty()) .is_some()) && (asset - .container_image_object_key + .ui_spritesheet_image_object_key .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) .is_some() + || asset + .ui_spritesheet_image_src + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .is_some() + || asset + .container_image_object_key + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .is_some() || asset .container_image_src .as_deref() @@ -1276,23 +1480,16 @@ pub(super) fn build_match3d_material_sheet_prompt( config: &Match3DConfigJson, item_names: &[String], ) -> String { - let asset_style_prompt = resolve_match3d_asset_style_prompt(config); - let style_clause = asset_style_prompt - .as_ref() - .map(|prompt| format!("整体画风遵循:{prompt}。")) - .unwrap_or_default(); - let item_rows = item_names - .iter() - .enumerate() - .map(|(index, name)| format!("第{}行:{name} 的 5 个不同视角", index + 1)) - .collect::>() - .join(";"); - format!( - "生成一张1024x1024的1:1图片。固定生成5行*5列网格素材图,画面是{theme}题材的抓大鹅游戏2D物品素材。{style_clause}严格5*5均匀排布,严格按行组织:{item_rows}。同一行五格必须是同一物品的五个不同视角,依次为正面、左前、右前、俯视、背面;每个格子一个独立居中的完整物体,每格背景必须是统一纯绿色绿幕背景(高饱和亮绿色,接近 #00FF00),背景平整无纹理、无渐变、无阴影、无道具,方便后续抠成透明。物体本身不得使用与绿幕相同的纯绿色;若物品天然含绿色,必须使用更深、更黄或更蓝的绿色并用清晰描边与绿幕区分。统一柔和光照,清晰轮廓,适合直接切割成游戏2D图标。请让每个物体完整落在自己的格子中央,四周保留留白,相邻物体主体之间必须至少保留单个素材格宽度的1/4空白间距(约25%单格宽度),包含左右相邻格和上下相邻行,物体主体不得占满格子。禁止主体跨格、贴边或越界,禁止任何内容进入相邻格子影响裁剪后的效果。不要出现文字、水印、UI、边框、网格线、标签、底座、场景或其他物体。", - theme = config.theme_text, - style_clause = style_clause, - item_rows = item_rows, - ) + let _ = (config, item_names); + build_match3d_item_spritesheet_prompt() +} + +pub(super) fn build_match3d_item_spritesheet_prompt() -> String { + "固定生成10行*10列spritesheet图,统一纯绿色绿幕背景(高饱和亮绿色,接近 #00FF00),背景平整无纹理、无渐变、无阴影、无场景内容,后端会在生图后将绿幕扣成透明并把透明背景 PNG 存到 OSS。素材间距严格均匀分布,任意两个素材间距相同,物品来自参考图中画面中心容器中的2D素材。每一行包含两种物品,每种物品的五个不同形态。物品素材自身不得使用接近 #00FF00 的高饱和纯绿;绿色物品只能使用深绿、橄榄绿、金绿或蓝绿,并用清晰描边与绿幕区分。严禁出现两种高相似度的物品".to_string() +} + +fn match3d_material_sheet_special_prompt() -> String { + "每一行包含两种物品,每种物品的五个不同形态。".to_string() } pub(super) fn build_match3d_material_sheet_negative_prompt(config: &Match3DConfigJson) -> String { @@ -1337,6 +1534,7 @@ fn is_match3d_pixel_retro_style(config: &Match3DConfigJson) -> bool { .is_some_and(|value| value.contains("像素复古")) } +#[cfg(test)] pub(super) fn slice_match3d_material_sheet( image: &DownloadedOpenAiImage, item_names: &[String], @@ -1447,966 +1645,23 @@ pub(super) fn crop_match3d_material_view_edge_matte( ) .to_image(), ) -} - -#[derive(Clone, Copy, Debug)] -struct Match3DMaterialCellBounds { - x0: u32, - y0: u32, - x1: u32, - y1: u32, -} - -impl Match3DMaterialCellBounds { - fn width(self) -> u32 { - self.x1.saturating_sub(self.x0).max(1) - } - - fn height(self) -> u32 { - self.y1.saturating_sub(self.y0).max(1) - } - - fn area(self) -> u32 { - self.width().saturating_mul(self.height()) - } - - fn to_crop_tuple(self) -> (u32, u32, u32, u32) { - (self.x0, self.y0, self.width(), self.height()) - } -} - -fn resolve_match3d_material_cell_bounds( - image_width: u32, - image_height: u32, - row_count: u32, - row: u32, - col: u32, -) -> Match3DMaterialCellBounds { - let normalized_rows = row_count.clamp(1, MATCH3D_MATERIAL_GRID_SIZE); - let cell_x0 = col.saturating_mul(image_width) / MATCH3D_MATERIAL_GRID_SIZE; - let cell_x1 = (col.saturating_add(1)).saturating_mul(image_width) / MATCH3D_MATERIAL_GRID_SIZE; - let cell_y0 = row.saturating_mul(image_height) / normalized_rows; - let cell_y1 = (row.saturating_add(1)).saturating_mul(image_height) / normalized_rows; - - Match3DMaterialCellBounds { - x0: cell_x0.min(image_width.saturating_sub(1)), - y0: cell_y0.min(image_height.saturating_sub(1)), - x1: cell_x1.clamp(cell_x0.saturating_add(1), image_width), - y1: cell_y1.clamp(cell_y0.saturating_add(1), image_height), - } -} - -fn detect_match3d_material_foreground_bounds( - source: &image::DynamicImage, - cell: Match3DMaterialCellBounds, -) -> Option { - let background = sample_match3d_material_cell_background(source, cell); - let mut foreground: Option = None; - let mut foreground_pixels = 0u32; - - for y in cell.y0..cell.y1 { - for x in cell.x0..cell.x1 { - if !is_match3d_material_foreground_pixel(source.get_pixel(x, y).0, background) { - continue; - } - foreground_pixels = foreground_pixels.saturating_add(1); - foreground = Some(match foreground { - Some(bounds) => Match3DMaterialCellBounds { - x0: bounds.x0.min(x), - y0: bounds.y0.min(y), - x1: bounds.x1.max(x.saturating_add(1)), - y1: bounds.y1.max(y.saturating_add(1)), - }, - None => Match3DMaterialCellBounds { - x0: x, - y0: y, - x1: x.saturating_add(1), - y1: y.saturating_add(1), - }, - }); - } - } - - let min_foreground_pixels = (cell.area() / 320).clamp(12, 220); - foreground.filter(|bounds| { - foreground_pixels >= min_foreground_pixels && bounds.width() > 2 && bounds.height() > 2 + .map(|rows| { + rows.into_iter() + .map(|views| { + views + .into_iter() + .map(|view| Match3DSlicedItemImage { bytes: view.bytes }) + .collect() + }) + .collect() }) } -fn detect_match3d_material_visible_bounds( - image: &image::RgbaImage, -) -> Option { - let (width, height) = image.dimensions(); - let mut bounds: Option = None; - let mut visible_pixels = 0u32; - - for y in 0..height { - for x in 0..width { - let pixel = image.get_pixel(x, y).0; - if !is_match3d_material_visible_pixel(pixel) { - continue; - } - visible_pixels = visible_pixels.saturating_add(1); - bounds = Some(match bounds { - Some(current) => Match3DMaterialCellBounds { - x0: current.x0.min(x), - y0: current.y0.min(y), - x1: current.x1.max(x.saturating_add(1)), - y1: current.y1.max(y.saturating_add(1)), - }, - None => Match3DMaterialCellBounds { - x0: x, - y0: y, - x1: x.saturating_add(1), - y1: y.saturating_add(1), - }, - }); - } - } - - let min_visible_pixels = ((width.saturating_mul(height)) / 540).clamp(10, 120); - bounds.filter(|visible_bounds| { - visible_pixels >= min_visible_pixels - && visible_bounds.width() > 2 - && visible_bounds.height() > 2 - }) -} - -fn sample_match3d_material_cell_background( - source: &image::DynamicImage, - cell: Match3DMaterialCellBounds, -) -> [u8; 4] { - let sample_size = (cell.width().min(cell.height()) / 12).clamp(2, 8); - let sample_points = [ - (cell.x0, cell.y0), - (cell.x1.saturating_sub(sample_size), cell.y0), - (cell.x0, cell.y1.saturating_sub(sample_size)), - ( - cell.x1.saturating_sub(sample_size), - cell.y1.saturating_sub(sample_size), - ), - ]; - let mut samples = Vec::new(); - for (start_x, start_y) in sample_points { - let mut totals = [0u32; 4]; - let mut count = 0u32; - for y in start_y..start_y.saturating_add(sample_size).min(cell.y1) { - for x in start_x..start_x.saturating_add(sample_size).min(cell.x1) { - let pixel = source.get_pixel(x, y).0; - totals[0] = totals[0].saturating_add(pixel[0] as u32); - totals[1] = totals[1].saturating_add(pixel[1] as u32); - totals[2] = totals[2].saturating_add(pixel[2] as u32); - totals[3] = totals[3].saturating_add(pixel[3] as u32); - count = count.saturating_add(1); - } - } - if count > 0 { - samples.push([ - (totals[0] / count) as u8, - (totals[1] / count) as u8, - (totals[2] / count) as u8, - (totals[3] / count) as u8, - ]); - } - } - - samples - .into_iter() - .min_by_key(|sample| { - let luminance = sample[0] as u16 + sample[1] as u16 + sample[2] as u16; - (sample[3] as u16, u16::MAX.saturating_sub(luminance)) - }) - .unwrap_or([255, 255, 255, 255]) -} - -fn clamp_match3d_material_unit(value: f32) -> f32 { - value.clamp(0.0, 1.0) -} - -fn lerp_match3d_material_channel(from: f32, to: f32, t: f32) -> f32 { - from + (to - from) * clamp_match3d_material_unit(t) -} - -fn is_match3d_material_foreground_pixel(pixel: [u8; 4], background: [u8; 4]) -> bool { - let alpha_diff = pixel[3] as i32 - background[3] as i32; - if alpha_diff.abs() >= MATCH3D_MATERIAL_FOREGROUND_ALPHA_THRESHOLD && pixel[3] > 24 { - return true; - } - if pixel[3] <= 24 { - return false; - } - - let color_diff = (pixel[0] as i32 - background[0] as i32).abs() - + (pixel[1] as i32 - background[1] as i32).abs() - + (pixel[2] as i32 - background[2] as i32).abs(); - color_diff >= MATCH3D_MATERIAL_FOREGROUND_DIFF_THRESHOLD -} - -fn remove_match3d_material_view_edge_matte(pixels: &mut [u8], width: usize, height: usize) -> bool { - let pixel_count = width.saturating_mul(height); - if pixel_count == 0 || pixels.len() < pixel_count.saturating_mul(4) { - return false; - } - - let mut changed = false; - let mut background_mask = vec![0u8; pixel_count]; - let mut queue = Vec::::new(); - let mut queue_index = 0usize; - let mut transparent_pixel_count = 0usize; - for pixel_index in 0..pixel_count { - let offset = pixel_index * 4; - if pixels[offset + 3] == 0 { - background_mask[pixel_index] = 1; - queue.push(pixel_index); - transparent_pixel_count = transparent_pixel_count.saturating_add(1); - } - } - let has_transparent_background = transparent_pixel_count > pixel_count / 200; - - // 中文注释:单图被前景边界收紧后,浅绿框可能正好贴在 PNG 外缘; - // 把外缘一段宽度作为去背种子,但只清理绿幕 / 近白 matte,避免误伤贴边主体。 - let edge_width = resolve_match3d_material_view_edge_cleanup_width(width, height); - for y in 0..height { - for x in 0..width { - if x >= edge_width - && y >= edge_width - && x.saturating_add(edge_width) < width - && y.saturating_add(edge_width) < height - { - continue; - } - let pixel_index = y * width + x; - if background_mask[pixel_index] != 0 { - continue; - } - let offset = pixel_index * 4; - let pixel = [ - pixels[offset], - pixels[offset + 1], - pixels[offset + 2], - pixels[offset + 3], - ]; - if !is_match3d_material_view_background_pixel(pixel) { - continue; - } - background_mask[pixel_index] = 1; - queue.push(pixel_index); - } - } - - while queue_index < queue.len() { - let pixel_index = queue[queue_index]; - queue_index += 1; - let x = pixel_index % width; - let y = pixel_index / width; - let neighbors = [ - (x > 0).then(|| pixel_index - 1), - (x + 1 < width).then_some(pixel_index + 1), - (y > 0).then(|| pixel_index - width), - (y + 1 < height).then_some(pixel_index + width), - ]; - - for next_pixel_index in neighbors.into_iter().flatten() { - if background_mask[next_pixel_index] != 0 { - continue; - } - let offset = next_pixel_index * 4; - let pixel = [ - pixels[offset], - pixels[offset + 1], - pixels[offset + 2], - pixels[offset + 3], - ]; - if !is_match3d_material_view_background_pixel(pixel) { - continue; - } - background_mask[next_pixel_index] = 1; - queue.push(next_pixel_index); - } - } - - for _ in 0..edge_width { - let mut expanded_mask = background_mask.clone(); - let mut changed_this_round = false; - for y in 0..height { - for x in 0..width { - let pixel_index = y * width + x; - if background_mask[pixel_index] != 0 { - continue; - } - let offset = pixel_index * 4; - if !is_match3d_material_view_background_pixel([ - pixels[offset], - pixels[offset + 1], - pixels[offset + 2], - pixels[offset + 3], - ]) { - continue; - } - - if touches_match3d_material_background_mask(x, y, width, height, &background_mask) { - expanded_mask[pixel_index] = 1; - changed_this_round = true; - } - } - } - background_mask = expanded_mask; - if !changed_this_round { - break; - } - } - - // 中文注释:边缘抗锯齿圈要直接从可见像素里剔除,再按剩余主体重新收紧裁边。 - for pixel_index in 0..pixel_count { - if background_mask[pixel_index] == 0 { - continue; - } - let offset = pixel_index * 4; - if pixels[offset + 3] != 0 - || pixels[offset] != 0 - || pixels[offset + 1] != 0 - || pixels[offset + 2] != 0 - { - pixels[offset] = 0; - pixels[offset + 1] = 0; - pixels[offset + 2] = 0; - pixels[offset + 3] = 0; - changed = true; - } - } - - if has_transparent_background { - let mut visible_mask = vec![0u8; pixel_count]; - for pixel_index in 0..pixel_count { - let offset = pixel_index * 4; - if is_match3d_material_visible_pixel([ - pixels[offset], - pixels[offset + 1], - pixels[offset + 2], - pixels[offset + 3], - ]) { - visible_mask[pixel_index] = 1; - } - } - - for _ in 0..2 { - let mut changed_this_round = false; - for y in 0..height { - for x in 0..width { - let pixel_index = y * width + x; - if visible_mask[pixel_index] == 0 { - continue; - } - let offset = pixel_index * 4; - let pixel = [ - pixels[offset], - pixels[offset + 1], - pixels[offset + 2], - pixels[offset + 3], - ]; - if !is_match3d_material_green_contaminated_edge_pixel(pixel) { - continue; - } - if !touches_match3d_material_background_mask( - x, - y, - width, - height, - &background_mask, - ) { - continue; - } - - if is_match3d_material_strong_green_contamination(pixel) { - pixels[offset] = 0; - pixels[offset + 1] = 0; - pixels[offset + 2] = 0; - pixels[offset + 3] = 0; - visible_mask[pixel_index] = 0; - background_mask[pixel_index] = 1; - changed = true; - changed_this_round = true; - continue; - } - - let replacement = collect_match3d_material_visible_neighbor_color( - pixels, - width, - height, - x, - y, - &background_mask, - &visible_mask, - ) - .unwrap_or(( - pixels[offset], - pixels[offset + 1], - pixels[offset + 2], - )); - let next_red = replacement.0.max(pixels[offset]); - let next_blue = replacement.2.max(pixels[offset + 2]); - let next_green = replacement - .1 - .min(next_red.max(next_blue).saturating_add(12)); - if next_red != pixels[offset] - || next_green != pixels[offset + 1] - || next_blue != pixels[offset + 2] - { - pixels[offset] = next_red; - pixels[offset + 1] = next_green; - pixels[offset + 2] = next_blue; - changed = true; - changed_this_round = true; - } - background_mask[pixel_index] = 1; - } - } - if !changed_this_round { - break; - } - } - } - - changed -} - -fn resolve_match3d_material_view_edge_cleanup_width(width: usize, height: usize) -> usize { - let min_side = width.min(height).max(1); - (min_side / 24).clamp(4, 12).min(min_side) -} - -fn is_match3d_material_view_background_pixel(pixel: [u8; 4]) -> bool { - pixel[3] < 16 - || is_match3d_material_soft_edge_pixel(pixel) - || compute_match3d_material_white_screen_score(pixel) > 0.18 -} - -fn is_match3d_material_visible_pixel(pixel: [u8; 4]) -> bool { - pixel[3] > 0 && (pixel[0] > 8 || pixel[1] > 8 || pixel[2] > 8) -} - -fn is_match3d_material_soft_edge_pixel(pixel: [u8; 4]) -> bool { - if pixel[3] == 0 { - return false; - } - - let red = pixel[0]; - let green = pixel[1]; - let blue = pixel[2]; - green >= 188 - && green.saturating_sub(red.max(blue)) >= 42 - && (red >= 48 || blue >= 96 || pixel[3] < 236) -} - -fn is_match3d_material_green_contaminated_edge_pixel(pixel: [u8; 4]) -> bool { - if pixel[3] == 0 { - return false; - } - - let red = pixel[0]; - let green = pixel[1]; - let blue = pixel[2]; - green >= 72 && green.saturating_sub(red.max(blue)) >= 18 -} - -fn is_match3d_material_strong_green_contamination(pixel: [u8; 4]) -> bool { - let red = pixel[0]; - let green = pixel[1]; - let blue = pixel[2]; - green >= 148 && green.saturating_sub(red.max(blue)) >= 34 -} - -fn collect_match3d_material_visible_neighbor_color( - pixels: &[u8], - width: usize, - height: usize, - x: usize, - y: usize, - background_mask: &[u8], - visible_mask: &[u8], -) -> Option<(u8, u8, u8)> { - let mut total_weight = 0.0f32; - let mut total_red = 0.0f32; - let mut total_green = 0.0f32; - let mut total_blue = 0.0f32; - - for offset_y in -3i32..=3 { - for offset_x in -3i32..=3 { - if offset_x == 0 && offset_y == 0 { - continue; - } - let next_x = x as i32 + offset_x; - let next_y = y as i32 + offset_y; - if next_x < 0 || next_x >= width as i32 || next_y < 0 || next_y >= height as i32 { - continue; - } - - let next_pixel_index = next_y as usize * width + next_x as usize; - if background_mask[next_pixel_index] != 0 || visible_mask[next_pixel_index] == 0 { - continue; - } - - let next_offset = next_pixel_index * 4; - let next_alpha = pixels[next_offset + 3]; - if next_alpha < 96 { - continue; - } - let pixel = [ - pixels[next_offset], - pixels[next_offset + 1], - pixels[next_offset + 2], - next_alpha, - ]; - if is_match3d_material_green_contaminated_edge_pixel(pixel) - || is_match3d_material_soft_edge_pixel(pixel) - { - continue; - } - - let distance = offset_x.unsigned_abs() + offset_y.unsigned_abs(); - let weight = (next_alpha as f32 / 255.0) - * if distance <= 1 { - 2.0 - } else if distance <= 3 { - 1.2 - } else { - 0.7 - }; - total_weight += weight; - total_red += pixels[next_offset] as f32 * weight; - total_green += pixels[next_offset + 1] as f32 * weight; - total_blue += pixels[next_offset + 2] as f32 * weight; - } - } - - if total_weight <= 0.0 { - return None; - } - - Some(( - (total_red / total_weight).round() as u8, - (total_green / total_weight).round() as u8, - (total_blue / total_weight).round() as u8, - )) -} - -fn apply_match3d_material_green_screen_alpha(source: image::DynamicImage) -> image::DynamicImage { - let mut image = source.to_rgba8(); - let (width, height) = image.dimensions(); - remove_match3d_material_green_screen_background( - image.as_mut(), - width as usize, - height as usize, - ); - image::DynamicImage::ImageRgba8(image) -} - -fn remove_match3d_material_green_screen_background( - pixels: &mut [u8], - width: usize, - height: usize, -) -> bool { - let pixel_count = width.saturating_mul(height); - if pixel_count == 0 || pixels.len() < pixel_count.saturating_mul(4) { - return false; - } - - let mut green_scores = vec![0.0f32; pixel_count]; - let mut white_scores = vec![0.0f32; pixel_count]; - let mut background_hints = vec![0.0f32; pixel_count]; - let mut background_mask = vec![0u8; pixel_count]; - let mut queue = Vec::::new(); - let mut queue_index = 0usize; - - for pixel_index in 0..pixel_count { - let offset = pixel_index * 4; - let red = pixels[offset]; - let green = pixels[offset + 1]; - let blue = pixels[offset + 2]; - let alpha = pixels[offset + 3]; - let green_score = compute_match3d_material_green_screen_score([red, green, blue, alpha]); - let white_score = compute_match3d_material_white_screen_score([red, green, blue, alpha]); - let transparency_hint = clamp_match3d_material_unit((56.0 - alpha as f32) / 56.0) * 0.75; - - green_scores[pixel_index] = green_score; - white_scores[pixel_index] = white_score; - background_hints[pixel_index] = green_score.max(white_score).max(transparency_hint); - } - - let seed_background_pixel = |pixel_index: usize, - background_mask: &mut [u8], - queue: &mut Vec| { - if background_mask[pixel_index] != 0 { - return; - } - let alpha = pixels[pixel_index * 4 + 3]; - let strong_candidate = alpha < 40 - || green_scores[pixel_index] >= MATCH3D_MATERIAL_HARD_GREEN_SCREEN_SCORE - || (alpha < 224 && green_scores[pixel_index] > MATCH3D_MATERIAL_GREEN_SCREEN_MIN_SCORE) - || white_scores[pixel_index] > 0.32; - if !strong_candidate { - return; - } - background_mask[pixel_index] = 1; - queue.push(pixel_index); - }; - - for x in 0..width { - seed_background_pixel(x, &mut background_mask, &mut queue); - seed_background_pixel((height - 1) * width + x, &mut background_mask, &mut queue); - } - for y in 1..height.saturating_sub(1) { - seed_background_pixel(y * width, &mut background_mask, &mut queue); - seed_background_pixel(y * width + width - 1, &mut background_mask, &mut queue); - } - - while queue_index < queue.len() { - let pixel_index = queue[queue_index]; - queue_index += 1; - - let x = pixel_index % width; - let y = pixel_index / width; - let neighbor_indexes = [ - if x > 0 { Some(pixel_index - 1) } else { None }, - if x + 1 < width { - Some(pixel_index + 1) - } else { - None - }, - if y > 0 { - Some(pixel_index - width) - } else { - None - }, - if y + 1 < height { - Some(pixel_index + width) - } else { - None - }, - ]; - - for next_pixel_index in neighbor_indexes.into_iter().flatten() { - if background_mask[next_pixel_index] != 0 { - continue; - } - let next_offset = next_pixel_index * 4; - let alpha = pixels[next_offset + 3]; - let green_score = green_scores[next_pixel_index]; - let white_score = white_scores[next_pixel_index]; - let hint = background_hints[next_pixel_index]; - let reachable_soft_edge = hint > 0.08 - && alpha < 224 - && (green_score > 0.04 || white_score > 0.08 || alpha < 180); - let green_background = green_score >= MATCH3D_MATERIAL_HARD_GREEN_SCREEN_SCORE - || (alpha < 224 && green_score > MATCH3D_MATERIAL_GREEN_SCREEN_MIN_SCORE); - if alpha < 40 || green_background || white_score > 0.32 || reachable_soft_edge { - background_mask[next_pixel_index] = 1; - queue.push(next_pixel_index); - } - } - } - - // 中文注释:Gemini 有时把每个素材格生成成独立绿幕块,块外又是近白背景; - // 这类绿幕不一定和整张 sheet 外边缘连通,必须用高置信绿幕直接补进背景层。 - for pixel_index in 0..pixel_count { - if background_mask[pixel_index] == 0 - && green_scores[pixel_index] >= MATCH3D_MATERIAL_HARD_GREEN_SCREEN_SCORE - { - background_mask[pixel_index] = 1; - } - } - - // 中文注释:较厚的抗锯齿绿边可能低于 hard 阈值;先沿整张 sheet 的透明背景向内吃掉 - // 软绿边,再进入格子裁剪,避免每张切图自带绿色描边。 - let soft_green_cleanup_rounds = (width.min(height) / 40).clamp(4, 14); - for _ in 0..soft_green_cleanup_rounds { - let mut expanded_mask = background_mask.clone(); - let mut changed_this_round = false; - for y in 0..height { - for x in 0..width { - let pixel_index = y * width + x; - if background_mask[pixel_index] != 0 { - continue; - } - let offset = pixel_index * 4; - let pixel = [ - pixels[offset], - pixels[offset + 1], - pixels[offset + 2], - pixels[offset + 3], - ]; - let green_score = green_scores[pixel_index]; - let white_score = white_scores[pixel_index]; - if !is_match3d_material_soft_green_matte_pixel(pixel, green_score, white_score) { - continue; - } - if !touches_match3d_material_background_mask(x, y, width, height, &background_mask) - { - continue; - } - - expanded_mask[pixel_index] = 1; - changed_this_round = true; - } - } - background_mask = expanded_mask; - if !changed_this_round { - break; - } - } - - // 中文注释:主体边缘常带一圈绿幕或白底抗锯齿,扩一层软边,避免切割后残留毛边。 - for _ in 0..2 { - let mut expanded_mask = background_mask.clone(); - for y in 0..height { - for x in 0..width { - let pixel_index = y * width + x; - if background_mask[pixel_index] != 0 { - continue; - } - let alpha = pixels[pixel_index * 4 + 3]; - let green_score = green_scores[pixel_index]; - let white_score = white_scores[pixel_index]; - let hint = background_hints[pixel_index]; - let soft_matte_candidate = alpha < 224 - || white_score > 0.10 - || green_score >= MATCH3D_MATERIAL_HARD_GREEN_SCREEN_SCORE; - if hint < MATCH3D_MATERIAL_GREEN_SCREEN_SOFT_SCORE || !soft_matte_candidate { - continue; - } - - let mut adjacent_background_count = 0usize; - for offset_y in -1i32..=1 { - for offset_x in -1i32..=1 { - if offset_x == 0 && offset_y == 0 { - continue; - } - let next_x = x as i32 + offset_x; - let next_y = y as i32 + offset_y; - if next_x < 0 - || next_x >= width as i32 - || next_y < 0 - || next_y >= height as i32 - { - adjacent_background_count += 1; - continue; - } - if background_mask[next_y as usize * width + next_x as usize] != 0 { - adjacent_background_count += 1; - } - } - } - - if adjacent_background_count >= 2 - || (adjacent_background_count >= 1 - && hint >= MATCH3D_MATERIAL_GREEN_SCREEN_MIN_SCORE) - { - expanded_mask[pixel_index] = 1; - } - } - } - background_mask = expanded_mask; - } - - let mut changed = false; - for pixel_index in 0..pixel_count { - if background_mask[pixel_index] == 0 { - continue; - } - let alpha_offset = pixel_index * 4 + 3; - if pixels[alpha_offset] != 0 { - pixels[alpha_offset] = 0; - changed = true; - } - } - - for y in 0..height { - for x in 0..width { - let pixel_index = y * width + x; - let offset = pixel_index * 4; - let alpha = pixels[offset + 3]; - if alpha == 0 { - continue; - } - - let mut touches_transparent_edge = false; - for offset_y in -1i32..=1 { - for offset_x in -1i32..=1 { - if offset_x == 0 && offset_y == 0 { - continue; - } - let next_x = x as i32 + offset_x; - let next_y = y as i32 + offset_y; - if next_x < 0 || next_x >= width as i32 || next_y < 0 || next_y >= height as i32 - { - touches_transparent_edge = true; - continue; - } - let next_pixel_index = next_y as usize * width + next_x as usize; - if background_mask[next_pixel_index] != 0 - || pixels[next_pixel_index * 4 + 3] < 16 - { - touches_transparent_edge = true; - } - } - } - - if !touches_transparent_edge { - continue; - } - - let green_score = green_scores[pixel_index]; - let white_score = white_scores[pixel_index]; - let contamination = green_score.max(white_score).max(if alpha < 220 { - ((220 - alpha) as f32 / 220.0) * 0.25 - } else { - 0.0 - }); - if contamination < 0.06 { - continue; - } - - let sample = collect_match3d_material_foreground_neighbor_color( - pixels, - width, - height, - x, - y, - &background_mask, - &background_hints, - ); - let mut red = pixels[offset] as f32; - let mut green = pixels[offset + 1] as f32; - let mut blue = pixels[offset + 2] as f32; - let blend = clamp_match3d_material_unit(contamination.max(0.22)); - - if let Some((sample_red, sample_green, sample_blue)) = sample { - red = lerp_match3d_material_channel(red, sample_red as f32, blend); - green = lerp_match3d_material_channel(green, sample_green as f32, blend); - blue = lerp_match3d_material_channel(blue, sample_blue as f32, blend); - - if green_score > 0.04 { - green = green.min(sample_green as f32 + 18.0); - } - if white_score > 0.1 { - red = red.min(sample_red as f32 + 26.0); - green = green.min(sample_green as f32 + 26.0); - blue = blue.min(sample_blue as f32 + 26.0); - } - } else { - if green_score > 0.04 { - let toned_green = (green - (green - red.max(blue)) * 0.78) - .round() - .max(red.max(blue)); - green = green.min(toned_green).min(red.max(blue) + 18.0); - } - - if white_score > 0.12 { - let spread = red.max(green).max(blue) - red.min(green).min(blue); - if spread < 20.0 { - let toned_value = ((red + green + blue) / 3.0 * 0.88).round(); - red = red.min(toned_value); - green = green.min(toned_value); - blue = blue.min(toned_value); - } - } - } - - let mut next_alpha = alpha; - let edge_fade = (green_score * 0.35).max(white_score * 0.28); - if edge_fade > 0.08 { - next_alpha = ((alpha as f32) * (1.0 - edge_fade)).round() as u8; - if next_alpha < 10 { - next_alpha = 0; - } - } - - let next_red = red.round().clamp(0.0, 255.0) as u8; - let next_green = green.round().clamp(0.0, 255.0) as u8; - let next_blue = blue.round().clamp(0.0, 255.0) as u8; - if next_red != pixels[offset] - || next_green != pixels[offset + 1] - || next_blue != pixels[offset + 2] - || next_alpha != alpha - { - pixels[offset] = next_red; - pixels[offset + 1] = next_green; - pixels[offset + 2] = next_blue; - pixels[offset + 3] = next_alpha; - changed = true; - } - } - } - - changed -} - -fn touches_match3d_material_background_mask( - x: usize, - y: usize, - width: usize, - height: usize, - background_mask: &[u8], -) -> bool { - for offset_y in -1i32..=1 { - for offset_x in -1i32..=1 { - if offset_x == 0 && offset_y == 0 { - continue; - } - let next_x = x as i32 + offset_x; - let next_y = y as i32 + offset_y; - if next_x < 0 || next_x >= width as i32 || next_y < 0 || next_y >= height as i32 { - return true; - } - if background_mask[next_y as usize * width + next_x as usize] != 0 { - return true; - } - } - } - false -} - -fn is_match3d_material_soft_green_matte_pixel( - pixel: [u8; 4], - green_score: f32, - white_score: f32, -) -> bool { - if pixel[3] == 0 || green_score < MATCH3D_MATERIAL_GREEN_SCREEN_MIN_SCORE { - return false; - } - - let red = pixel[0]; - let green = pixel[1]; - let blue = pixel[2]; - let foreground_mix = red.max(blue); - green >= 188 - && white_score < 0.34 - && green.saturating_sub(foreground_mix) >= 42 - && (red >= 48 || blue >= 96 || pixel[3] < 236) -} - -fn compute_match3d_material_green_screen_score(pixel: [u8; 4]) -> f32 { - if pixel[3] == 0 { - return 1.0; - } - - let red = pixel[0] as f32; - let green = pixel[1] as f32; - let blue = pixel[2] as f32; - let green_lead = green - red.max(blue); - if green < 96.0 || green_lead <= 18.0 { - return 0.0; - } - - let green_ratio = green / (red + blue).max(1.0); - if green_ratio <= 0.9 { - return 0.0; - } - - (((green - 96.0) / 128.0).clamp(0.0, 1.0) * 0.34 - + ((green_lead - 18.0) / 120.0).clamp(0.0, 1.0) * 0.46 - + ((green_ratio - 0.9) / 2.4).clamp(0.0, 1.0) * 0.20) - .clamp(0.0, 1.0) +#[cfg(test)] +pub(super) fn crop_match3d_material_view_edge_matte( + image: image::DynamicImage, +) -> image::DynamicImage { + crop_generated_asset_sheet_view_edge_matte(image) } fn compute_match3d_material_white_screen_score(pixel: [u8; 4]) -> f32 { @@ -2425,12 +1680,11 @@ fn compute_match3d_material_white_screen_score(pixel: [u8; 4]) -> f32 { } let spread = max_channel - min_channel; - let neutrality = 1.0 - clamp_match3d_material_unit((spread - 6.0) / 34.0); - let brightness = clamp_match3d_material_unit((average - 188.0) / 55.0); - let floor = clamp_match3d_material_unit((min_channel - 168.0) / 60.0); - clamp_match3d_material_unit(neutrality * (brightness * 0.85 + floor * 0.15)) + let neutrality = 1.0 - ((spread - 6.0) / 34.0).clamp(0.0, 1.0); + let brightness = ((average - 188.0) / 55.0).clamp(0.0, 1.0); + let floor = ((min_channel - 168.0) / 60.0).clamp(0.0, 1.0); + (neutrality * (brightness * 0.85 + floor * 0.15)).clamp(0.0, 1.0) } - pub(super) fn remove_match3d_container_plain_background( pixels: &mut [u8], width: usize, @@ -2570,67 +1824,3 @@ fn is_match3d_container_background_pixel(pixel: [u8; 4]) -> bool { fn is_match3d_container_soft_background_pixel(pixel: [u8; 4]) -> bool { pixel[3] < 80 || compute_match3d_material_white_screen_score(pixel) > 0.18 } - -fn collect_match3d_material_foreground_neighbor_color( - pixels: &[u8], - width: usize, - height: usize, - x: usize, - y: usize, - background_mask: &[u8], - background_hints: &[f32], -) -> Option<(u8, u8, u8)> { - let mut total_weight = 0.0f32; - let mut total_red = 0.0f32; - let mut total_green = 0.0f32; - let mut total_blue = 0.0f32; - - for offset_y in -2i32..=2 { - for offset_x in -2i32..=2 { - if offset_x == 0 && offset_y == 0 { - continue; - } - let next_x = x as i32 + offset_x; - let next_y = y as i32 + offset_y; - if next_x < 0 || next_x >= width as i32 || next_y < 0 || next_y >= height as i32 { - continue; - } - - let next_pixel_index = next_y as usize * width + next_x as usize; - if background_mask[next_pixel_index] != 0 || background_hints[next_pixel_index] >= 0.18 - { - continue; - } - - let next_offset = next_pixel_index * 4; - let next_alpha = pixels[next_offset + 3]; - if next_alpha < 96 { - continue; - } - let distance = offset_x.unsigned_abs() + offset_y.unsigned_abs(); - let weight = (next_alpha as f32 / 255.0) - * if distance <= 1 { - 1.8 - } else if distance == 2 { - 1.2 - } else { - 0.7 - }; - - total_weight += weight; - total_red += pixels[next_offset] as f32 * weight; - total_green += pixels[next_offset + 1] as f32 * weight; - total_blue += pixels[next_offset + 2] as f32 * weight; - } - } - - if total_weight <= 0.0 { - return None; - } - - Some(( - (total_red / total_weight).round() as u8, - (total_green / total_weight).round() as u8, - (total_blue / total_weight).round() as u8, - )) -} diff --git a/server-rs/crates/api-server/src/match3d/mappers.rs b/server-rs/crates/api-server/src/match3d/mappers.rs index c3b0067e..3fe49eb6 100644 --- a/server-rs/crates/api-server/src/match3d/mappers.rs +++ b/server-rs/crates/api-server/src/match3d/mappers.rs @@ -282,13 +282,25 @@ pub(super) fn map_match3d_image_view_from_work( pub(super) fn map_match3d_background_asset_for_agent( asset: Match3DGeneratedBackgroundAsset, ) -> shared_contracts::match3d_agent::Match3DGeneratedBackgroundAssetResponse { + let ui_spritesheet_image_src = asset.ui_spritesheet_image_src.clone(); + let ui_spritesheet_image_object_key = asset.ui_spritesheet_image_object_key.clone(); shared_contracts::match3d_agent::Match3DGeneratedBackgroundAssetResponse { prompt: asset.prompt, + level_scene_prompt: asset.level_scene_prompt, + level_scene_image_src: asset.level_scene_image_src, + level_scene_image_object_key: asset.level_scene_image_object_key, image_src: asset.image_src, image_object_key: asset.image_object_key, + ui_spritesheet_prompt: asset.ui_spritesheet_prompt, + ui_spritesheet_image_src: ui_spritesheet_image_src.clone(), + ui_spritesheet_image_object_key: ui_spritesheet_image_object_key.clone(), + item_spritesheet_prompt: asset.item_spritesheet_prompt, + item_spritesheet_image_src: asset.item_spritesheet_image_src, + item_spritesheet_image_object_key: asset.item_spritesheet_image_object_key, container_prompt: asset.container_prompt, - container_image_src: asset.container_image_src, - container_image_object_key: asset.container_image_object_key, + container_image_src: ui_spritesheet_image_src.or(asset.container_image_src), + container_image_object_key: ui_spritesheet_image_object_key + .or(asset.container_image_object_key), status: asset.status, error: asset.error, } @@ -299,8 +311,17 @@ pub(super) fn map_match3d_background_asset_for_work( ) -> shared_contracts::match3d_works::Match3DGeneratedBackgroundAssetResponse { shared_contracts::match3d_works::Match3DGeneratedBackgroundAssetResponse { prompt: asset.prompt, + level_scene_prompt: asset.level_scene_prompt, + level_scene_image_src: asset.level_scene_image_src, + level_scene_image_object_key: asset.level_scene_image_object_key, image_src: asset.image_src, image_object_key: asset.image_object_key, + ui_spritesheet_prompt: asset.ui_spritesheet_prompt, + ui_spritesheet_image_src: asset.ui_spritesheet_image_src, + ui_spritesheet_image_object_key: asset.ui_spritesheet_image_object_key, + item_spritesheet_prompt: asset.item_spritesheet_prompt, + item_spritesheet_image_src: asset.item_spritesheet_image_src, + item_spritesheet_image_object_key: asset.item_spritesheet_image_object_key, container_prompt: asset.container_prompt, container_image_src: asset.container_image_src, container_image_object_key: asset.container_image_object_key, @@ -327,6 +348,14 @@ pub(super) fn resolve_match3d_default_cover_image_src( .map(str::trim) .filter(|value| !value.is_empty()) .map(str::to_string) + .or_else(|| { + asset + .ui_spritesheet_image_src + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(str::to_string) + }) .or_else(|| { asset .container_image_object_key @@ -335,6 +364,14 @@ pub(super) fn resolve_match3d_default_cover_image_src( .filter(|value| !value.is_empty()) .map(str::to_string) }) + .or_else(|| { + asset + .ui_spritesheet_image_object_key + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(str::to_string) + }) .or_else(|| { asset .image_src @@ -408,6 +445,10 @@ fn match3d_item_asset_has_image(asset: &Match3DGeneratedItemAssetJson) -> bool { fn match3d_background_asset_has_image(asset: &Match3DGeneratedBackgroundAsset) -> bool { match3d_text_present(asset.image_src.as_ref()) || match3d_text_present(asset.image_object_key.as_ref()) + || match3d_text_present(asset.ui_spritesheet_image_src.as_ref()) + || match3d_text_present(asset.ui_spritesheet_image_object_key.as_ref()) + || match3d_text_present(asset.item_spritesheet_image_src.as_ref()) + || match3d_text_present(asset.item_spritesheet_image_object_key.as_ref()) || match3d_text_present(asset.container_image_src.as_ref()) || match3d_text_present(asset.container_image_object_key.as_ref()) } diff --git a/server-rs/crates/api-server/src/match3d/vector_engine_gemini.rs b/server-rs/crates/api-server/src/match3d/vector_engine_gemini.rs index b19e89c3..c3a078a6 100644 --- a/server-rs/crates/api-server/src/match3d/vector_engine_gemini.rs +++ b/server-rs/crates/api-server/src/match3d/vector_engine_gemini.rs @@ -26,6 +26,9 @@ pub(super) async fn generate_match3d_material_sheet( Ok(Match3DMaterialSheet { task_id: generated.task_id, + prompt, + image_src: None, + image_object_key: None, image, }) } diff --git a/server-rs/crates/api-server/src/match3d/works.rs b/server-rs/crates/api-server/src/match3d/works.rs index 67bbe7eb..2a710519 100644 --- a/server-rs/crates/api-server/src/match3d/works.rs +++ b/server-rs/crates/api-server/src/match3d/works.rs @@ -212,7 +212,7 @@ pub(super) async fn ensure_match3d_background_asset( } } - let generated_background = generate_match3d_background_image( + let generated_background = generate_match3d_level_asset_bundle( state, owner_user_id, session_id, @@ -236,6 +236,40 @@ pub(super) async fn ensure_match3d_background_asset( Ok(assets) } +pub(super) async fn resolve_or_generate_match3d_level_asset_bundle( + state: &AppState, + request_context: &RequestContext, + owner_user_id: &str, + session_id: &str, + profile_id: &str, + config: &Match3DConfigJson, + background_prompt: &str, + assets: &[Match3DGeneratedItemAsset], +) -> Result { + if let Some(existing_background) = find_match3d_generated_background_asset(assets) { + if is_match3d_background_asset_ready(&existing_background) { + return Ok(existing_background); + } + } + + let normalized_prompt = normalize_match3d_background_prompt(background_prompt); + let resolved_prompt = if normalized_prompt.is_empty() { + build_fallback_match3d_background_prompt(config) + } else { + normalized_prompt + }; + generate_match3d_level_asset_bundle( + state, + owner_user_id, + session_id, + profile_id, + config, + resolved_prompt.as_str(), + ) + .await + .map_err(|error| match3d_error_response(request_context, MATCH3D_AGENT_PROVIDER, error)) +} + pub(super) fn attach_match3d_background_asset_to_assets( assets: &mut Vec, background_asset: Match3DGeneratedBackgroundAsset, @@ -281,7 +315,7 @@ pub(super) async fn generate_match3d_cover_image_asset( create_openai_image_edit( &http_client, &settings, - build_match3d_cover_edit_prompt(cover_prompt.as_str()).as_str(), + build_match3d_cover_uploaded_reference_prompt(cover_prompt.as_str()).as_str(), Some("文字、水印、UI、按钮、倒计时、分数、教程浮层、菜单、边框"), "1:1", &uploaded_image, @@ -289,27 +323,38 @@ pub(super) async fn generate_match3d_cover_image_asset( ) .await? } else { - let reference_images = resolve_match3d_cover_reference_image_data_urls( + let reference_images = resolve_match3d_cover_reference_images_for_edit( state, reference_image_srcs, MATCH3D_ITEM_IMAGE_MAX_BYTES, ) .await?; - create_openai_image_generation( - &http_client, - &settings, - build_match3d_cover_reference_generation_prompt( + if reference_images.is_empty() { + create_openai_image_generation( + &http_client, + &settings, cover_prompt.as_str(), - !reference_images.is_empty(), + Some("文字、水印、UI、按钮、倒计时、分数、教程浮层、菜单、边框"), + "1:1", + 1, + &[], + "抓大鹅封面图生成失败", ) - .as_str(), - Some("文字、水印、UI、按钮、倒计时、分数、教程浮层、菜单、边框"), - "1:1", - 1, - reference_images.as_slice(), - "抓大鹅封面图生成失败", - ) - .await? + .await? + } else { + create_openai_image_edit_with_references( + &http_client, + &settings, + build_match3d_cover_reference_generation_prompt(cover_prompt.as_str(), true) + .as_str(), + Some("文字、水印、UI、按钮、倒计时、分数、教程浮层、菜单、边框"), + "1:1", + 1, + reference_images.as_slice(), + "抓大鹅封面图生成失败", + ) + .await? + } }; let image = generated.images.into_iter().next().ok_or_else(|| { AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ @@ -347,7 +392,7 @@ fn build_match3d_cover_generation_prompt(config: &Match3DConfigJson, prompt: &st ) } -pub(super) fn build_match3d_cover_edit_prompt(prompt: &str) -> String { +pub(super) fn build_match3d_cover_uploaded_reference_prompt(prompt: &str) -> String { format!( concat!( "请以随请求上传的封面图作为第一优先级重绘依据,保留主图的主体、构图、视角和主要配色;", @@ -382,24 +427,113 @@ pub(super) async fn generate_match3d_background_image( profile_id: &str, config: &Match3DConfigJson, prompt: &str, +) -> Result { + generate_match3d_level_asset_bundle( + state, + owner_user_id, + session_id, + profile_id, + config, + prompt, + ) + .await +} + +pub(super) async fn generate_match3d_level_asset_bundle( + state: &AppState, + owner_user_id: &str, + session_id: &str, + profile_id: &str, + config: &Match3DConfigJson, + prompt: &str, ) -> Result { require_match3d_oss_client(state)?; let settings = require_openai_image_settings(state)?; let http_client = build_openai_image_http_client(&settings)?; - let reference_image = load_match3d_container_reference_image().await?; - let generated_background = create_openai_image_generation( + + let level_scene_prompt = build_match3d_level_scene_generation_prompt(config); + let generated_scene = create_openai_image_generation( &http_client, &settings, - build_match3d_background_generation_prompt(config, prompt).as_str(), - Some( - "文字、水印、UI、按钮、倒计时、分数、物品、角色、手、边框、教程浮层、菜单、透明区域、透明 alpha、镂空、棋盘格透明底", - ), + level_scene_prompt.as_str(), + Some("水印、教程浮层、菜单、广告、真实手机外框、浏览器 UI"), "9:16", 1, &[], - "抓大鹅背景图生成失败", + "抓大鹅关卡画面生成失败", ) .await?; + let level_scene_image = generated_scene.images.into_iter().next().ok_or_else(|| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "vector-engine", + "message": "抓大鹅关卡画面生成失败:未返回图片", + })) + })?; + let level_scene_reference = OpenAiReferenceImage { + bytes: level_scene_image.bytes.clone(), + mime_type: level_scene_image.mime_type.clone(), + file_name: "match3d-level-scene.png".to_string(), + }; + let level_scene_upload = persist_match3d_generated_bytes( + state, + owner_user_id, + session_id, + profile_id, + &["level-scene", generated_scene.task_id.as_str()], + "scene.png", + level_scene_image.mime_type.as_str(), + level_scene_image.bytes, + "match3d_level_scene_image", + Some(generated_scene.task_id.as_str()), + current_utc_micros(), + ) + .await?; + + let ui_prompt = build_match3d_ui_spritesheet_prompt(); + let background_extract_prompt = build_match3d_background_from_scene_prompt(); + let generated_ui_future = create_openai_image_edit( + &http_client, + &settings, + ui_prompt.as_str(), + Some("整页背景、中心物品、容器内物品、重复按钮、文字说明、白底、纯色底、网格线"), + "1:1", + &level_scene_reference, + "抓大鹅 UI spritesheet 生成失败", + ); + let generated_background_future = create_openai_image_edit( + &http_client, + &settings, + background_extract_prompt.as_str(), + Some("返回按钮、设置按钮、倒计时、标题文字、道具按钮、物品、容器内含物、菜单、教程浮层"), + "9:16", + &level_scene_reference, + "抓大鹅背景图生成失败", + ); + let (generated_ui, generated_background) = + tokio::try_join!(generated_ui_future, generated_background_future)?; + + let ui_image = generated_ui.images.into_iter().next().ok_or_else(|| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "vector-engine", + "message": "抓大鹅 UI spritesheet 生成失败:未返回图片", + })) + })?; + let ui_image = make_match3d_spritesheet_image_transparent(ui_image)?; + let ui_upload = persist_match3d_generated_bytes( + state, + owner_user_id, + session_id, + profile_id, + &["ui-spritesheet", generated_ui.task_id.as_str()], + "ui-spritesheet.png", + ui_image.mime_type.as_str(), + ui_image.bytes, + "match3d_ui_spritesheet_image", + Some(generated_ui.task_id.as_str()), + current_utc_micros(), + ) + .await?; + let background_image = generated_background .images .into_iter() @@ -426,50 +560,22 @@ pub(super) async fn generate_match3d_background_image( ) .await?; - let container_prompt = build_match3d_container_generation_prompt(config, prompt); - let generated_container = create_openai_image_edit( - &http_client, - &settings, - container_prompt.as_str(), - Some("文字、水印、按钮、倒计时、分数、物品、角色、手、教程浮层、菜单、整页背景、小容器、正俯视圆盘、侧视碗、餐盘、托盘、画布大留白"), - "1:1", - &reference_image, - "抓大鹅容器 UI 图生成失败", - ) - .await?; - let container_image = generated_container - .images - .into_iter() - .next() - .ok_or_else(|| { - AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ - "provider": "vector-engine", - "message": "抓大鹅容器 UI 图生成失败:未返回图片", - })) - })?; - let container_image = make_match3d_container_image_transparent(container_image)?; - let container_upload = persist_match3d_generated_bytes( - state, - owner_user_id, - session_id, - profile_id, - &["ui-container", generated_container.task_id.as_str()], - "container.png", - container_image.mime_type.as_str(), - container_image.bytes, - "match3d_ui_container_image", - Some(generated_container.task_id.as_str()), - current_utc_micros(), - ) - .await?; - Ok(Match3DGeneratedBackgroundAsset { prompt: prompt.to_string(), + level_scene_prompt: Some(level_scene_prompt), + level_scene_image_src: Some(level_scene_upload.src), + level_scene_image_object_key: Some(level_scene_upload.object_key), image_src: Some(background_upload.src), image_object_key: Some(background_upload.object_key), - container_prompt: Some(container_prompt), - container_image_src: Some(container_upload.src), - container_image_object_key: Some(container_upload.object_key), + ui_spritesheet_prompt: Some(ui_prompt.clone()), + ui_spritesheet_image_src: Some(ui_upload.src.clone()), + ui_spritesheet_image_object_key: Some(ui_upload.object_key.clone()), + item_spritesheet_prompt: None, + item_spritesheet_image_src: None, + item_spritesheet_image_object_key: None, + container_prompt: Some(ui_prompt), + container_image_src: Some(ui_upload.src), + container_image_object_key: Some(ui_upload.object_key), status: "image_ready".to_string(), error: None, }) @@ -486,7 +592,7 @@ pub(super) async fn generate_match3d_container_image( require_match3d_oss_client(state)?; let settings = require_openai_image_settings(state)?; let http_client = build_openai_image_http_client(&settings)?; - let reference_image = load_match3d_container_reference_image().await?; + let reference_image = load_match3d_container_reference_image()?; let container_prompt = build_match3d_container_generation_prompt(config, prompt); let generated_container = create_openai_image_edit( &http_client, @@ -533,6 +639,7 @@ pub(super) async fn generate_match3d_container_image( container_image_object_key: Some(container_upload.object_key), status: "image_ready".to_string(), error: None, + ..Default::default() }) } @@ -549,12 +656,39 @@ pub(super) fn merge_match3d_container_image_into_background_asset( .unwrap_or_else(|| container_asset.prompt.clone()); Match3DGeneratedBackgroundAsset { prompt, + level_scene_prompt: existing_background + .as_ref() + .and_then(|asset| asset.level_scene_prompt.clone()), + level_scene_image_src: existing_background + .as_ref() + .and_then(|asset| asset.level_scene_image_src.clone()), + level_scene_image_object_key: existing_background + .as_ref() + .and_then(|asset| asset.level_scene_image_object_key.clone()), image_src: existing_background .as_ref() .and_then(|asset| asset.image_src.clone()), image_object_key: existing_background .as_ref() .and_then(|asset| asset.image_object_key.clone()), + ui_spritesheet_prompt: existing_background + .as_ref() + .and_then(|asset| asset.ui_spritesheet_prompt.clone()), + ui_spritesheet_image_src: existing_background + .as_ref() + .and_then(|asset| asset.ui_spritesheet_image_src.clone()), + ui_spritesheet_image_object_key: existing_background + .as_ref() + .and_then(|asset| asset.ui_spritesheet_image_object_key.clone()), + item_spritesheet_prompt: existing_background + .as_ref() + .and_then(|asset| asset.item_spritesheet_prompt.clone()), + item_spritesheet_image_src: existing_background + .as_ref() + .and_then(|asset| asset.item_spritesheet_image_src.clone()), + item_spritesheet_image_object_key: existing_background + .as_ref() + .and_then(|asset| asset.item_spritesheet_image_object_key.clone()), container_prompt: container_asset.container_prompt, container_image_src: container_asset.container_image_src, container_image_object_key: container_asset.container_image_object_key, @@ -563,15 +697,10 @@ pub(super) fn merge_match3d_container_image_into_background_asset( } } -async fn load_match3d_container_reference_image() -> Result { - let bytes = tokio::fs::read(MATCH3D_CONTAINER_REFERENCE_IMAGE_PATH) - .await - .map_err(|error| { - AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({ - "provider": MATCH3D_AGENT_PROVIDER, - "message": format!("读取抓大鹅容器参考图失败:{error}"), - })) - })?; +pub(super) fn load_match3d_container_reference_image() -> Result { + // 中文注释:生产 API 单独发布二进制,Web 静态资源可能在另一轮流水线发布。 + // 容器参考图属于后端生图协议输入,必须随 api-server 编译进二进制,不能依赖运行时 cwd 下存在 public/。 + let bytes = MATCH3D_CONTAINER_REFERENCE_IMAGE_BYTES.to_vec(); if bytes.is_empty() { return Err( AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({ @@ -766,6 +895,32 @@ pub(super) fn make_match3d_container_image_transparent( extension: "png".to_string(), }) } + +pub(super) fn make_match3d_spritesheet_image_transparent( + image: DownloadedOpenAiImage, +) -> Result { + let source = image::load_from_memory(image.bytes.as_slice()).map_err(|error| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "match3d-assets", + "message": format!("抓大鹅 spritesheet 图解码失败:{error}"), + })) + })?; + let mut encoded = std::io::Cursor::new(Vec::new()); + apply_generated_asset_sheet_green_screen_alpha(source) + .write_to(&mut encoded, ImageFormat::Png) + .map_err(|error| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "match3d-assets", + "message": format!("抓大鹅 spritesheet 图透明化失败:{error}"), + })) + })?; + + Ok(DownloadedOpenAiImage { + bytes: encoded.into_inner(), + mime_type: "image/png".to_string(), + extension: "png".to_string(), + }) +} pub(super) async fn download_match3d_legacy_model( file: &hyper3d_contract::Hyper3dDownloadFilePayload, ) -> Result { @@ -869,7 +1024,7 @@ pub(super) fn is_match3d_glb_binary_payload(bytes: &[u8]) -> bool { magic == 0x4654_6c67 && version == 2 && declared_length == bytes.len() } -async fn read_match3d_generated_object_bytes( +pub(super) async fn read_match3d_generated_object_bytes( state: &AppState, object_key: &str, message_prefix: &str, @@ -920,57 +1075,6 @@ async fn read_match3d_generated_object_bytes( Ok(bytes.to_vec()) } -async fn resolve_match3d_reference_image_data_url( - state: &AppState, - source: Option<&str>, - max_size_bytes: usize, -) -> Result, AppError> { - let Some(source) = source.map(str::trim).filter(|value| !value.is_empty()) else { - return Ok(None); - }; - if source.starts_with("data:image/") { - return Ok(Some(source.to_string())); - } - if let Some(public_path) = normalize_match3d_public_reference_image_path(source) { - let bytes = tokio::fs::read(public_path.as_str()) - .await - .map_err(|error| { - AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({ - "provider": MATCH3D_WORKS_PROVIDER, - "message": format!("读取抓大鹅本地参考图失败:{error}"), - "path": public_path, - })) - })?; - if bytes.is_empty() || bytes.len() > max_size_bytes { - return Err( - AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ - "provider": MATCH3D_WORKS_PROVIDER, - "field": "referenceImageSrcs", - "message": "封面参考图过大,请压缩后重试。", - "maxBytes": max_size_bytes, - "actualBytes": bytes.len(), - })), - ); - } - return Ok(Some(format!( - "data:{};base64,{}", - infer_match3d_image_mime_type(bytes.as_slice()), - BASE64_STANDARD.encode(bytes) - ))); - } - if !source.trim_start_matches('/').starts_with("generated-") { - return Ok(Some(source.to_string())); - } - let bytes = - read_match3d_generated_object_bytes(state, source, "读取抓大鹅参考图失败", max_size_bytes) - .await?; - Ok(Some(format!( - "data:{};base64,{}", - infer_match3d_image_mime_type(bytes.as_slice()), - BASE64_STANDARD.encode(bytes) - ))) -} - pub(super) fn normalize_match3d_public_reference_image_path(source: &str) -> Option { let source = source .trim() @@ -992,7 +1096,9 @@ pub(super) fn normalize_match3d_public_reference_image_path(source: &str) -> Opt ) { return None; } - Some(format!("public/{source}")) + Some(format!( + "{MATCH3D_PUBLIC_REFERENCE_IMAGE_PATH_PREFIX}{source}" + )) } pub(super) fn collect_match3d_cover_reference_image_sources( @@ -1021,18 +1127,22 @@ pub(super) fn collect_match3d_cover_reference_image_sources( sources } -async fn resolve_match3d_cover_reference_image_data_urls( +async fn resolve_match3d_cover_reference_images_for_edit( state: &AppState, sources: Vec, max_size_bytes: usize, -) -> Result, AppError> { +) -> Result, AppError> { let mut resolved = Vec::new(); - for source in sources { - if let Some(data_url) = - resolve_match3d_reference_image_data_url(state, Some(source.as_str()), max_size_bytes) - .await? + for (index, source) in sources.into_iter().enumerate() { + if let Some(image) = resolve_match3d_reference_image_for_edit( + state, + Some(source.as_str()), + max_size_bytes, + format!("match3d-cover-reference-{index}").as_str(), + ) + .await? { - resolved.push(data_url); + resolved.push(image); } } Ok(resolved) @@ -1049,6 +1159,16 @@ async fn resolve_match3d_reference_image_for_edit( }; let bytes = if source.starts_with("data:image/") { decode_match3d_data_url_bytes(source)? + } else if let Some(public_path) = normalize_match3d_public_reference_image_path(source) { + tokio::fs::read(public_path.as_str()) + .await + .map_err(|error| { + AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({ + "provider": MATCH3D_WORKS_PROVIDER, + "message": format!("读取抓大鹅本地参考图失败:{error}"), + "path": public_path, + })) + })? } else if source.trim_start_matches('/').starts_with("generated-") { read_match3d_generated_object_bytes( state, @@ -1062,7 +1182,7 @@ async fn resolve_match3d_reference_image_for_edit( AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": MATCH3D_WORKS_PROVIDER, "field": "uploadedImageSrc", - "message": "封面上传图必须是图片 Data URL 或 /generated-* 路径。", + "message": "封面参考图必须是图片 Data URL、本地 public 参考图或 /generated-* 路径。", })), ); }; @@ -1089,7 +1209,7 @@ async fn resolve_match3d_reference_image_for_edit( })) } -fn decode_match3d_data_url_bytes(source: &str) -> Result, AppError> { +pub(super) fn decode_match3d_data_url_bytes(source: &str) -> Result, AppError> { let Some((header, data)) = source.split_once(',') else { return Err( AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ diff --git a/server-rs/crates/api-server/src/modules.rs b/server-rs/crates/api-server/src/modules.rs index 17a75784..a86abae9 100644 --- a/server-rs/crates/api-server/src/modules.rs +++ b/server-rs/crates/api-server/src/modules.rs @@ -7,6 +7,7 @@ pub mod custom_world; pub mod edutainment; pub mod health; pub mod internal; +pub mod jump_hop; pub mod match3d; pub mod platform; pub mod profile; diff --git a/server-rs/crates/api-server/src/modules/jump_hop.rs b/server-rs/crates/api-server/src/modules/jump_hop.rs new file mode 100644 index 00000000..7648fe91 --- /dev/null +++ b/server-rs/crates/api-server/src/modules/jump_hop.rs @@ -0,0 +1,76 @@ +use axum::{ + Router, middleware, + routing::{get, post}, +}; + +use crate::{ + auth::require_bearer_auth, + jump_hop::{ + create_jump_hop_session, execute_jump_hop_action, get_jump_hop_gallery_detail, + get_jump_hop_runtime_work, get_jump_hop_session, jump_hop_run_jump, list_jump_hop_gallery, + publish_jump_hop_work, restart_jump_hop_run, start_jump_hop_run, + }, + state::AppState, +}; + +pub fn router(state: AppState) -> Router { + Router::new() + .route( + "/api/creation/jump-hop/sessions", + post(create_jump_hop_session).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/creation/jump-hop/sessions/{session_id}", + get(get_jump_hop_session).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/creation/jump-hop/sessions/{session_id}/actions", + post(execute_jump_hop_action).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/creation/jump-hop/works/{profile_id}/publish", + post(publish_jump_hop_work).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/jump-hop/works/{profile_id}", + get(get_jump_hop_runtime_work), + ) + .route( + "/api/runtime/jump-hop/runs", + post(start_jump_hop_run).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/jump-hop/runs/{run_id}/jump", + post(jump_hop_run_jump).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/jump-hop/runs/{run_id}/restart", + post(restart_jump_hop_run).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route("/api/runtime/jump-hop/gallery", get(list_jump_hop_gallery)) + .route( + "/api/runtime/jump-hop/gallery/{public_work_code}", + get(get_jump_hop_gallery_detail), + ) +} diff --git a/server-rs/crates/api-server/src/modules/puzzle.rs b/server-rs/crates/api-server/src/modules/puzzle.rs index 55197b0d..fc2e18cb 100644 --- a/server-rs/crates/api-server/src/modules/puzzle.rs +++ b/server-rs/crates/api-server/src/modules/puzzle.rs @@ -1,6 +1,6 @@ use axum::{ Router, - extract::DefaultBodyLimit, + extract::{DefaultBodyLimit, FromRef}, middleware, routing::{get, post}, }; @@ -17,12 +17,13 @@ use crate::{ submit_puzzle_agent_message, submit_puzzle_leaderboard, swap_puzzle_pieces, update_puzzle_run_pause, use_puzzle_runtime_prop, }, - state::AppState, + state::{AppState, PuzzleApiState}, }; const PUZZLE_REFERENCE_IMAGE_BODY_LIMIT_BYTES: usize = 12 * 1024 * 1024; pub fn router(state: AppState) -> Router { + // 中文注释:拼图 handler 只接收 PuzzleApiState,鉴权层仍使用全局 AppState。 Router::new() .route( "/api/runtime/puzzle/agent/sessions", @@ -181,4 +182,6 @@ pub fn router(state: AppState) -> Router { require_bearer_auth, )), ) + .with_state(PuzzleApiState::from_ref(&state)) + .with_state(state) } diff --git a/server-rs/crates/api-server/src/openai_image_generation.rs b/server-rs/crates/api-server/src/openai_image_generation.rs index 55554701..3516948e 100644 --- a/server-rs/crates/api-server/src/openai_image_generation.rs +++ b/server-rs/crates/api-server/src/openai_image_generation.rs @@ -1,21 +1,44 @@ -use std::time::Duration; +use std::{error::Error, time::Duration}; use axum::http::StatusCode; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; use reqwest::header; use serde_json::{Map, Value, json}; -use crate::{http_error::AppError, state::AppState}; +use crate::{ + external_api_audit::{ + ExternalApiFailureDraft, app_error_status_class, is_retryable_external_api_failure, + record_external_api_failure, + }, + http_error::AppError, + state::AppState, +}; pub(crate) const GPT_IMAGE_2_MODEL: &str = "gpt-image-2"; -pub(crate) const VECTOR_ENGINE_GPT_IMAGE_2_MODEL: &str = "gpt-image-2-all"; +pub(crate) const VECTOR_ENGINE_GPT_IMAGE_2_MODEL: &str = GPT_IMAGE_2_MODEL; const VECTOR_ENGINE_PROVIDER: &str = "vector-engine"; -#[derive(Clone, Debug)] +#[derive(Clone)] pub(crate) struct OpenAiImageSettings { pub base_url: String, pub api_key: String, pub request_timeout_ms: u64, + pub external_api_audit_state: Option, +} + +impl std::fmt::Debug for OpenAiImageSettings { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("OpenAiImageSettings") + .field("base_url", &self.base_url) + .field("api_key", &"") + .field("request_timeout_ms", &self.request_timeout_ms) + .field( + "external_api_audit_enabled", + &self.external_api_audit_state.is_some(), + ) + .finish() + } } #[derive(Clone, Debug)] @@ -39,7 +62,7 @@ pub(crate) struct OpenAiReferenceImage { pub file_name: String, } -// 中文注释:RPG、方洞等图片资产统一走 VectorEngine GPT-image-2-all,避免把密钥或供应商协议暴露到前端。 +// 中文注释:RPG、方洞等图片资产统一走后端 VectorEngine GPT-image-2,避免把密钥或供应商协议暴露到前端。 pub(crate) fn require_openai_image_settings( state: &AppState, ) -> Result { @@ -74,6 +97,7 @@ pub(crate) fn require_openai_image_settings( base_url: base_url.to_string(), api_key: api_key.to_string(), request_timeout_ms: state.config.vector_engine_image_request_timeout_ms.max(1), + external_api_audit_state: Some(state.clone()), }) } @@ -82,7 +106,7 @@ pub(crate) fn build_openai_image_http_client( ) -> Result { reqwest::Client::builder() .timeout(Duration::from_millis(settings.request_timeout_ms)) - // 中文注释:同一客户端也会承载 `/v1/images/edits` multipart 图生图请求,强制 HTTP/1.1 可避开部分网关对 HTTP/2 multipart 流的兼容问题。 + // 中文注释:参考图会走 multipart edits;强制 HTTP/1.1 可避开部分网关对长耗时上传流的兼容问题。 .http1_only() .build() .map_err(|error| { @@ -103,15 +127,34 @@ pub(crate) async fn create_openai_image_generation( reference_images: &[String], failure_context: &str, ) -> Result { + if !reference_images.is_empty() { + let resolved_references = + resolve_openai_reference_images(http_client, reference_images, failure_context).await?; + return create_openai_image_edit_with_references( + http_client, + settings, + prompt, + negative_prompt, + size, + candidate_count, + resolved_references.as_slice(), + failure_context, + ) + .await; + } + + let request_url = vector_engine_images_generation_url(settings); + let normalized_size = normalize_image_size(size); let request_body = build_openai_image_request_body( prompt, negative_prompt, - size, + normalized_size.as_str(), candidate_count, reference_images, ); - let response = http_client - .post(vector_engine_images_generation_url(settings)) + let started_at = std::time::Instant::now(); + let response = match http_client + .post(request_url.as_str()) .header( header::AUTHORIZATION, format!("Bearer {}", settings.api_key), @@ -121,16 +164,106 @@ pub(crate) async fn create_openai_image_generation( .json(&request_body) .send() .await - .map_err(|error| { - map_openai_image_request_error(format!( - "{failure_context}:创建图片生成任务失败:{error}" - )) - })?; + { + Ok(response) => response, + Err(error) => { + let latency_ms = started_at.elapsed().as_millis() as u64; + let timeout = error.is_timeout(); + let connect = error.is_connect(); + let source = error.source().map(ToString::to_string); + let message = format!("{failure_context}:创建图片生成任务失败:{error}"); + record_openai_image_failure_if_configured( + settings, + build_openai_image_failure_audit_draft( + request_url.as_str(), + failure_context, + "request_send", + None, + None, + timeout, + connect, + message.as_str(), + source, + None, + Some(latency_ms), + Some(prompt.chars().count()), + Some(reference_images.len()), + ), + ) + .await; + return Err(map_openai_image_reqwest_error( + format!("{failure_context}:创建图片生成任务失败").as_str(), + request_url.as_str(), + error, + )); + } + }; let response_status = response.status(); - let response_text = response.text().await.map_err(|error| { - map_openai_image_request_error(format!("{failure_context}:读取图片生成响应失败:{error}")) - })?; + tracing::info!( + provider = VECTOR_ENGINE_PROVIDER, + endpoint = %request_url, + status = response_status.as_u16(), + prompt_chars = prompt.chars().count(), + size = %normalized_size, + reference_image_count = reference_images.len(), + elapsed_ms = started_at.elapsed().as_millis() as u64, + failure_context, + "VectorEngine 图片生成 HTTP 返回" + ); + let response_text = match response.text().await { + Ok(response_text) => response_text, + Err(error) => { + let latency_ms = started_at.elapsed().as_millis() as u64; + let timeout = error.is_timeout(); + let connect = error.is_connect(); + let source = error.source().map(ToString::to_string); + let message = format!("{failure_context}:读取图片生成响应失败:{error}"); + record_openai_image_failure_if_configured( + settings, + build_openai_image_failure_audit_draft( + request_url.as_str(), + failure_context, + "response_body", + Some(response_status.as_u16()), + None, + timeout, + connect, + message.as_str(), + source, + None, + Some(latency_ms), + Some(prompt.chars().count()), + Some(reference_images.len()), + ), + ) + .await; + return Err(map_openai_image_reqwest_error( + format!("{failure_context}:读取图片生成响应失败").as_str(), + request_url.as_str(), + error, + )); + } + }; if !response_status.is_success() { + record_openai_image_failure_if_configured( + settings, + build_openai_image_failure_audit_draft( + request_url.as_str(), + failure_context, + "upstream_status", + Some(response_status.as_u16()), + None, + false, + false, + parse_api_error_message(response_text.as_str(), failure_context).as_str(), + None, + Some(truncate_raw(response_text.as_str())), + Some(started_at.elapsed().as_millis() as u64), + Some(prompt.chars().count()), + Some(reference_images.len()), + ), + ) + .await; return Err(map_openai_image_upstream_error( response_status.as_u16(), response_text.as_str(), @@ -138,26 +271,114 @@ pub(crate) async fn create_openai_image_generation( )); } - let response_json = parse_json_payload(response_text.as_str(), failure_context)?; + let response_json = match parse_json_payload(response_text.as_str(), failure_context) { + Ok(response_json) => response_json, + Err(error) => { + record_openai_image_failure_if_configured( + settings, + build_openai_image_failure_audit_draft( + request_url.as_str(), + failure_context, + "response_parse", + Some(response_status.as_u16()), + None, + false, + false, + error.body_text().as_str(), + None, + Some(truncate_raw(response_text.as_str())), + Some(started_at.elapsed().as_millis() as u64), + Some(prompt.chars().count()), + Some(reference_images.len()), + ), + ) + .await; + return Err(error); + } + }; let generation_id = extract_generation_id(&response_json.payload) .unwrap_or_else(|| format!("vector-engine-{}", current_utc_micros())); let actual_prompt = find_first_string_by_key(&response_json.payload, "revised_prompt") .or_else(|| find_first_string_by_key(&response_json.payload, "actual_prompt")); let image_urls = extract_image_urls(&response_json.payload); if !image_urls.is_empty() { - let mut generated = - download_images_from_urls(http_client, generation_id, image_urls, candidate_count) - .await?; + let download_started_at = std::time::Instant::now(); + let mut generated = match download_images_from_urls( + http_client, + generation_id, + image_urls, + candidate_count, + ) + .await + { + Ok(generated) => generated, + Err(error) => { + record_openai_image_failure_if_configured( + settings, + build_openai_image_failure_audit_draft( + request_url.as_str(), + failure_context, + "image_download", + Some(response_status.as_u16()), + Some(app_error_status_class(error.status_code())), + false, + false, + error.body_text().as_str(), + None, + None, + Some(download_started_at.elapsed().as_millis() as u64), + Some(prompt.chars().count()), + Some(reference_images.len()), + ), + ) + .await; + return Err(error); + } + }; generated.actual_prompt = actual_prompt; + tracing::info!( + provider = VECTOR_ENGINE_PROVIDER, + endpoint = %request_url, + image_count = generated.images.len(), + elapsed_ms = download_started_at.elapsed().as_millis() as u64, + failure_context, + "VectorEngine 图片下载完成" + ); return Ok(generated); } let b64_images = extract_b64_images(&response_json.payload); if !b64_images.is_empty() { let mut generated = images_from_base64(generation_id, b64_images, candidate_count); generated.actual_prompt = actual_prompt; + tracing::info!( + provider = VECTOR_ENGINE_PROVIDER, + endpoint = %request_url, + image_count = generated.images.len(), + failure_context, + "VectorEngine 图片 base64 解码完成" + ); return Ok(generated); } + record_openai_image_failure_if_configured( + settings, + build_openai_image_failure_audit_draft( + request_url.as_str(), + failure_context, + "missing_image", + Some(response_status.as_u16()), + None, + false, + false, + format!("{failure_context}:VectorEngine 未返回图片地址").as_str(), + None, + Some(truncate_raw(response_text.as_str())), + Some(started_at.elapsed().as_millis() as u64), + Some(prompt.chars().count()), + Some(reference_images.len()), + ), + ) + .await; Err( AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ "provider": VECTOR_ENGINE_PROVIDER, @@ -175,24 +396,65 @@ pub(crate) async fn create_openai_image_edit( reference_image: &OpenAiReferenceImage, failure_context: &str, ) -> Result { - let task_id = format!("vector-engine-edit-{}", current_utc_micros()); - let image_part = reqwest::multipart::Part::bytes(reference_image.bytes.clone()) - .file_name(reference_image.file_name.clone()) - .mime_str(reference_image.mime_type.as_str()) - .map_err(|error| { - map_openai_image_request_error(format!("{failure_context}:构造参考图失败:{error}")) - })?; - let form = reqwest::multipart::Form::new() - .part("image", image_part) + create_openai_image_edit_with_references( + http_client, + settings, + prompt, + negative_prompt, + size, + 1, + std::slice::from_ref(reference_image), + failure_context, + ) + .await +} + +pub(crate) async fn create_openai_image_edit_with_references( + http_client: &reqwest::Client, + settings: &OpenAiImageSettings, + prompt: &str, + negative_prompt: Option<&str>, + size: &str, + candidate_count: u32, + reference_images: &[OpenAiReferenceImage], + failure_context: &str, +) -> Result { + if reference_images.is_empty() { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": VECTOR_ENGINE_PROVIDER, + "message": format!("{failure_context}:图片编辑需要至少一张参考图。"), + })), + ); + } + + let request_url = vector_engine_images_edit_url(settings); + let normalized_size = normalize_image_size(size); + let mut form = reqwest::multipart::Form::new() .text("model", GPT_IMAGE_2_MODEL.to_string()) .text( "prompt", build_prompt_with_negative(prompt, negative_prompt), ) - .text("n", "1") - .text("size", normalize_image_size(size)); - let response = http_client - .post(vector_engine_images_edit_url(settings).as_str()) + .text("n", candidate_count.clamp(1, 4).to_string()) + .text("size", normalized_size.clone()); + + for reference_image in reference_images.iter().take(5) { + let image_part = reqwest::multipart::Part::bytes(reference_image.bytes.clone()) + .file_name(reference_image.file_name.clone()) + .mime_str(reference_image.mime_type.as_str()) + .map_err(|error| { + map_openai_image_request_error(format!( + "{failure_context}:构造参考图失败:{error}" + )) + })?; + form = form.part("image", image_part); + } + + let reference_image_count = reference_images.iter().take(5).count(); + let started_at = std::time::Instant::now(); + let response = match http_client + .post(request_url.as_str()) .header( header::AUTHORIZATION, format!("Bearer {}", settings.api_key), @@ -201,16 +463,106 @@ pub(crate) async fn create_openai_image_edit( .multipart(form) .send() .await - .map_err(|error| { - map_openai_image_request_error(format!( - "{failure_context}:创建图片编辑任务失败:{error}" - )) - })?; + { + Ok(response) => response, + Err(error) => { + let latency_ms = started_at.elapsed().as_millis() as u64; + let timeout = error.is_timeout(); + let connect = error.is_connect(); + let source = error.source().map(ToString::to_string); + let message = format!("{failure_context}:创建图片编辑任务失败:{error}"); + record_openai_image_failure_if_configured( + settings, + build_openai_image_failure_audit_draft( + request_url.as_str(), + failure_context, + "request_send", + None, + None, + timeout, + connect, + message.as_str(), + source, + None, + Some(latency_ms), + Some(prompt.chars().count()), + Some(reference_image_count), + ), + ) + .await; + return Err(map_openai_image_reqwest_error( + format!("{failure_context}:创建图片编辑任务失败").as_str(), + request_url.as_str(), + error, + )); + } + }; let response_status = response.status(); - let response_text = response.text().await.map_err(|error| { - map_openai_image_request_error(format!("{failure_context}:读取图片编辑响应失败:{error}")) - })?; + tracing::info!( + provider = VECTOR_ENGINE_PROVIDER, + endpoint = %request_url, + status = response_status.as_u16(), + prompt_chars = prompt.chars().count(), + size = %normalized_size, + reference_image_count, + elapsed_ms = started_at.elapsed().as_millis() as u64, + failure_context, + "VectorEngine 图片编辑 HTTP 返回" + ); + let response_text = match response.text().await { + Ok(response_text) => response_text, + Err(error) => { + let latency_ms = started_at.elapsed().as_millis() as u64; + let timeout = error.is_timeout(); + let connect = error.is_connect(); + let source = error.source().map(ToString::to_string); + let message = format!("{failure_context}:读取图片编辑响应失败:{error}"); + record_openai_image_failure_if_configured( + settings, + build_openai_image_failure_audit_draft( + request_url.as_str(), + failure_context, + "response_body", + Some(response_status.as_u16()), + None, + timeout, + connect, + message.as_str(), + source, + None, + Some(latency_ms), + Some(prompt.chars().count()), + Some(reference_image_count), + ), + ) + .await; + return Err(map_openai_image_reqwest_error( + format!("{failure_context}:读取图片编辑响应失败").as_str(), + request_url.as_str(), + error, + )); + } + }; if !response_status.is_success() { + record_openai_image_failure_if_configured( + settings, + build_openai_image_failure_audit_draft( + request_url.as_str(), + failure_context, + "upstream_status", + Some(response_status.as_u16()), + None, + false, + false, + parse_api_error_message(response_text.as_str(), failure_context).as_str(), + None, + Some(truncate_raw(response_text.as_str())), + Some(started_at.elapsed().as_millis() as u64), + Some(prompt.chars().count()), + Some(reference_image_count), + ), + ) + .await; return Err(map_openai_image_upstream_error( response_status.as_u16(), response_text.as_str(), @@ -218,26 +570,103 @@ pub(crate) async fn create_openai_image_edit( )); } - let response_json = parse_json_payload(response_text.as_str(), failure_context)?; + let response_json = match parse_json_payload(response_text.as_str(), failure_context) { + Ok(response_json) => response_json, + Err(error) => { + record_openai_image_failure_if_configured( + settings, + build_openai_image_failure_audit_draft( + request_url.as_str(), + failure_context, + "response_parse", + Some(response_status.as_u16()), + None, + false, + false, + error.body_text().as_str(), + None, + Some(truncate_raw(response_text.as_str())), + Some(started_at.elapsed().as_millis() as u64), + Some(prompt.chars().count()), + Some(reference_image_count), + ), + ) + .await; + return Err(error); + } + }; + let task_id = extract_generation_id(&response_json.payload) + .unwrap_or_else(|| format!("vector-engine-edit-{}", current_utc_micros())); let actual_prompt = find_first_string_by_key(&response_json.payload, "revised_prompt") .or_else(|| find_first_string_by_key(&response_json.payload, "actual_prompt")); let image_urls = extract_image_urls(&response_json.payload); if !image_urls.is_empty() { - let mut generated = download_images_from_urls(http_client, task_id, image_urls, 1).await?; + let download_started_at = std::time::Instant::now(); + let mut generated = match download_images_from_urls( + http_client, + task_id, + image_urls, + candidate_count, + ) + .await + { + Ok(generated) => generated, + Err(error) => { + record_openai_image_failure_if_configured( + settings, + build_openai_image_failure_audit_draft( + request_url.as_str(), + failure_context, + "image_download", + Some(response_status.as_u16()), + Some(app_error_status_class(error.status_code())), + false, + false, + error.body_text().as_str(), + None, + None, + Some(download_started_at.elapsed().as_millis() as u64), + Some(prompt.chars().count()), + Some(reference_image_count), + ), + ) + .await; + return Err(error); + } + }; generated.actual_prompt = actual_prompt; return Ok(generated); } let b64_images = extract_b64_images(&response_json.payload); if !b64_images.is_empty() { - let mut generated = images_from_base64(task_id, b64_images, 1); + let mut generated = images_from_base64(task_id, b64_images, candidate_count); generated.actual_prompt = actual_prompt; return Ok(generated); } + record_openai_image_failure_if_configured( + settings, + build_openai_image_failure_audit_draft( + request_url.as_str(), + failure_context, + "missing_image", + Some(response_status.as_u16()), + None, + false, + false, + format!("{failure_context}:VectorEngine 未返回编辑图片").as_str(), + None, + Some(truncate_raw(response_text.as_str())), + Some(started_at.elapsed().as_millis() as u64), + Some(prompt.chars().count()), + Some(reference_image_count), + ), + ) + .await; Err( AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ "provider": VECTOR_ENGINE_PROVIDER, - "message": format!("{failure_context}:VectorEngine 未返回编辑图片"), + "message": format!("{failure_context}:VectorEngine 未返回图片"), })), ) } @@ -247,12 +676,12 @@ pub(crate) fn build_openai_image_request_body( negative_prompt: Option<&str>, size: &str, candidate_count: u32, - reference_images: &[String], + _reference_images: &[String], ) -> Value { - let mut body = Map::from_iter([ + let body = Map::from_iter([ ( "model".to_string(), - Value::String(VECTOR_ENGINE_GPT_IMAGE_2_MODEL.to_string()), + Value::String(GPT_IMAGE_2_MODEL.to_string()), ), ( "prompt".to_string(), @@ -265,10 +694,6 @@ pub(crate) fn build_openai_image_request_body( ), ]); - if !reference_images.is_empty() { - body.insert("image".to_string(), json!(reference_images)); - } - Value::Object(body) } @@ -380,6 +805,100 @@ pub(crate) async fn download_remote_image( }) } +async fn resolve_openai_reference_images( + http_client: &reqwest::Client, + reference_images: &[String], + failure_context: &str, +) -> Result, AppError> { + let mut resolved = Vec::new(); + for (index, source) in reference_images.iter().take(5).enumerate() { + let source = source.trim(); + if source.is_empty() { + continue; + } + if let Some(reference_image) = parse_openai_reference_image_data_url(source, index)? { + resolved.push(reference_image); + continue; + } + if source.starts_with("http://") || source.starts_with("https://") { + let downloaded = download_remote_image(http_client, source) + .await + .map_err(|error| { + map_openai_image_request_error(format!( + "{failure_context}:下载参考图失败:{}", + error.body_text() + )) + })?; + resolved.push(OpenAiReferenceImage { + bytes: downloaded.bytes, + mime_type: downloaded.mime_type.clone(), + file_name: format!( + "reference-{index}.{}", + mime_to_extension(downloaded.mime_type.as_str()) + ), + }); + continue; + } + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": VECTOR_ENGINE_PROVIDER, + "message": format!("{failure_context}:参考图必须是图片 Data URL 或 HTTP(S) URL。"), + })), + ); + } + + if resolved.is_empty() { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": VECTOR_ENGINE_PROVIDER, + "message": format!("{failure_context}:图片编辑需要至少一张参考图。"), + })), + ); + } + + Ok(resolved) +} + +fn parse_openai_reference_image_data_url( + source: &str, + index: usize, +) -> Result, AppError> { + let Some(body) = source.strip_prefix("data:") else { + return Ok(None); + }; + let Some((mime_type, data)) = body.split_once(";base64,") else { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": VECTOR_ENGINE_PROVIDER, + "message": "参考图 Data URL 必须是 base64 图片。", + })), + ); + }; + if !mime_type.starts_with("image/") { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": VECTOR_ENGINE_PROVIDER, + "message": "参考图 Data URL 必须是图片类型。", + })), + ); + } + let bytes = BASE64_STANDARD.decode(data.trim()).map_err(|error| { + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": VECTOR_ENGINE_PROVIDER, + "message": format!("参考图 Data URL 解码失败:{error}"), + })) + })?; + let mime_type = normalize_downloaded_image_mime_type(mime_type); + Ok(Some(OpenAiReferenceImage { + bytes, + file_name: format!( + "reference-{index}.{}", + mime_to_extension(mime_type.as_str()) + ), + mime_type, + })) +} + fn parse_json_payload( raw_text: &str, failure_context: &str, @@ -402,6 +921,44 @@ fn map_openai_image_request_error(message: String) -> AppError { })) } +fn map_openai_image_reqwest_error( + context: &str, + request_url: &str, + error: reqwest::Error, +) -> AppError { + let is_timeout = error.is_timeout(); + let is_connect = error.is_connect(); + let source = error.source().map(ToString::to_string).unwrap_or_default(); + let message = format!("{context}:{error}"); + let status = if is_timeout { + StatusCode::GATEWAY_TIMEOUT + } else { + StatusCode::BAD_GATEWAY + }; + tracing::warn!( + provider = VECTOR_ENGINE_PROVIDER, + endpoint = %request_url, + timeout = is_timeout, + connect = is_connect, + request = error.is_request(), + body = error.is_body(), + source = %source, + message = %message, + "VectorEngine 图片请求发送失败" + ); + + AppError::from_status(status).with_details(json!({ + "provider": VECTOR_ENGINE_PROVIDER, + "message": message, + "endpoint": request_url, + "timeout": is_timeout, + "connect": is_connect, + "request": error.is_request(), + "body": error.is_body(), + "source": source, + })) +} + fn map_openai_image_upstream_error( upstream_status: u16, raw_text: &str, @@ -423,6 +980,53 @@ fn map_openai_image_upstream_error( })) } +async fn record_openai_image_failure_if_configured( + settings: &OpenAiImageSettings, + draft: ExternalApiFailureDraft, +) { + if let Some(state) = settings.external_api_audit_state.as_ref() { + record_external_api_failure(state, draft).await; + } +} + +fn build_openai_image_failure_audit_draft( + request_url: &str, + failure_context: &str, + failure_stage: &'static str, + status_code: Option, + status_class: Option<&'static str>, + timeout: bool, + connect: bool, + error_message: &str, + error_source: Option, + raw_excerpt: Option, + latency_ms: Option, + prompt_chars: Option, + reference_image_count: Option, +) -> ExternalApiFailureDraft { + ExternalApiFailureDraft::new( + VECTOR_ENGINE_PROVIDER, + request_url.to_string(), + failure_context.to_string(), + failure_stage, + error_message.to_string(), + ) + .with_status_code(status_code) + .with_optional_status_class(status_class) + .with_timeout(timeout) + .with_retryable(is_retryable_external_api_failure( + status_code, + timeout, + connect, + )) + .with_error_source(error_source) + .with_raw_excerpt(raw_excerpt) + .with_latency_ms(latency_ms) + .with_prompt_chars(prompt_chars) + .with_reference_image_count(reference_image_count) + .with_image_model(Some(VECTOR_ENGINE_GPT_IMAGE_2_MODEL)) +} + fn parse_api_error_message(raw_text: &str, fallback_message: &str) -> String { if raw_text.trim().is_empty() { return fallback_message.to_string(); @@ -606,7 +1210,7 @@ mod tests { use super::*; #[test] - fn gpt_image_2_request_uses_vector_engine_contract() { + fn gpt_image_2_generation_request_uses_create_model_without_reference_images() { let body = build_openai_image_request_body( "雾海神殿", Some("文字,水印"), @@ -615,25 +1219,52 @@ mod tests { &["data:image/png;base64,abcd".to_string()], ); - assert_eq!(body["model"], VECTOR_ENGINE_GPT_IMAGE_2_MODEL); + assert_eq!(body["model"], GPT_IMAGE_2_MODEL); assert_eq!(body["size"], "1536x1024"); assert_eq!(body["n"], 2); assert!(body.get("official_fallback").is_none()); - assert_eq!(body["image"][0], "data:image/png;base64,abcd"); + assert!(body.get("image").is_none()); assert!(body["prompt"].as_str().unwrap_or_default().contains("避免")); } #[test] - fn vector_engine_edit_url_uses_images_edits_endpoint() { + fn vector_engine_generation_url_normalizes_base_url() { let root_settings = OpenAiImageSettings { base_url: "https://vector.example".to_string(), api_key: "test-key".to_string(), request_timeout_ms: 1_000_000, + external_api_audit_state: None, }; let v1_settings = OpenAiImageSettings { base_url: "https://vector.example/v1".to_string(), api_key: "test-key".to_string(), request_timeout_ms: 1_000_000, + external_api_audit_state: None, + }; + + assert_eq!( + vector_engine_images_generation_url(&root_settings), + "https://vector.example/v1/images/generations" + ); + assert_eq!( + vector_engine_images_generation_url(&v1_settings), + "https://vector.example/v1/images/generations" + ); + } + + #[test] + fn vector_engine_edit_url_normalizes_base_url() { + let root_settings = OpenAiImageSettings { + base_url: "https://vector.example".to_string(), + api_key: "test-key".to_string(), + request_timeout_ms: 1_000_000, + external_api_audit_state: None, + }; + let v1_settings = OpenAiImageSettings { + base_url: "https://vector.example/v1".to_string(), + api_key: "test-key".to_string(), + request_timeout_ms: 1_000_000, + external_api_audit_state: None, }; assert_eq!( @@ -646,6 +1277,21 @@ mod tests { ); } + #[test] + fn reference_data_url_resolves_to_edit_image_part() { + let source = format!( + "data:image/png;base64,{}", + BASE64_STANDARD.encode(b"pngbytes") + ); + let image = parse_openai_reference_image_data_url(source.as_str(), 2) + .expect("data url should parse") + .expect("data url should resolve image"); + + assert_eq!(image.bytes, b"pngbytes"); + assert_eq!(image.mime_type, "image/png"); + assert_eq!(image.file_name, "reference-2.png"); + } + #[test] fn b64_json_response_decodes_png_image() { let images = images_from_base64( @@ -658,4 +1304,41 @@ mod tests { assert_eq!(images.images[0].mime_type, "image/png"); assert_eq!(images.images[0].extension, "png"); } + + #[test] + fn vector_engine_upstream_failure_builds_tracking_ready_audit_event() { + let audit = build_openai_image_failure_audit_draft( + "https://vector.example/v1/images/generations", + "拼图 UI 背景图生成失败", + "upstream_status", + Some(429), + None, + false, + false, + "上游限流", + None, + Some("{\"error\":\"rate limited\"}".to_string()), + Some(321), + Some(42), + Some(1), + ); + let tracking = crate::external_api_audit::build_external_api_failure_tracking_draft(&audit); + + assert_eq!( + tracking.event_key, + crate::external_api_audit::EXTERNAL_API_FAILURE_EVENT_KEY + ); + assert_eq!(tracking.scope_id, VECTOR_ENGINE_PROVIDER); + assert_eq!(tracking.metadata["provider"], VECTOR_ENGINE_PROVIDER); + assert_eq!(tracking.metadata["statusCode"], 429); + assert_eq!(tracking.metadata["statusClass"], "4xx"); + assert_eq!(tracking.metadata["failureStage"], "upstream_status"); + assert_eq!(tracking.metadata["retryable"], true); + assert_eq!(tracking.metadata["promptChars"], 42); + assert_eq!(tracking.metadata["referenceImageCount"], 1); + assert_eq!( + tracking.metadata["imageModel"], + VECTOR_ENGINE_GPT_IMAGE_2_MODEL + ); + } } diff --git a/server-rs/crates/api-server/src/prompt/puzzle/draft.rs b/server-rs/crates/api-server/src/prompt/puzzle/draft.rs index f1eb2b9b..913b9dc8 100644 --- a/server-rs/crates/api-server/src/prompt/puzzle/draft.rs +++ b/server-rs/crates/api-server/src/prompt/puzzle/draft.rs @@ -40,9 +40,11 @@ pub(crate) fn resolve_puzzle_draft_cover_prompt( pub(crate) fn resolve_puzzle_level_image_prompt( explicit_prompt: Option<&str>, level_picture_description: &str, + draft_summary: &str, ) -> String { normalize_prompt_part(explicit_prompt) .or_else(|| normalize_prompt_part(Some(level_picture_description))) + .or_else(|| normalize_prompt_part(Some(draft_summary))) .unwrap_or_default() .to_string() } @@ -76,8 +78,15 @@ mod tests { #[test] fn level_image_prompt_falls_back_to_level_description() { - let prompt = resolve_puzzle_level_image_prompt(Some(" "), "关卡画面描述"); + let prompt = resolve_puzzle_level_image_prompt(Some(" "), "关卡画面描述", "作品简介"); assert_eq!(prompt, "关卡画面描述"); } + + #[test] + fn level_image_prompt_falls_back_to_draft_summary_like_initial_cover() { + let prompt = resolve_puzzle_level_image_prompt(Some(" "), " ", "作品简介"); + + assert_eq!(prompt, "作品简介"); + } } diff --git a/server-rs/crates/api-server/src/prompt/puzzle/image.rs b/server-rs/crates/api-server/src/prompt/puzzle/image.rs index 667e7bcd..8a7ae996 100644 --- a/server-rs/crates/api-server/src/prompt/puzzle/image.rs +++ b/server-rs/crates/api-server/src/prompt/puzzle/image.rs @@ -43,7 +43,7 @@ fn build_puzzle_image_prompt_text(_level_name: &str, prompt: &str) -> String { concat!( "请生成一张高清插画。", "画面主体:{prompt}。", - "画面要求:1:1", + "画面要求:输出画面比例为1:1,", "主体要清晰集中,前中后景层次明确,局部细节丰富但不要杂乱,", "避免文字、水印、边框和 UI 元素。" ), @@ -77,7 +77,7 @@ mod tests { let prompt = build_puzzle_image_prompt("雨夜神庙", "猫咪在发光遗迹前寻找线索"); assert!(prompt.contains("猫咪在发光遗迹前寻找线索")); - assert!(prompt.contains("1:1")); + assert!(prompt.contains("输出画面比例为1:1")); assert!(prompt.contains("主体要清晰集中")); assert!(prompt.contains("避免文字、水印、边框和 UI 元素")); } @@ -90,7 +90,7 @@ mod tests { let prompt = build_puzzle_image_prompt(long_level_name.as_str(), long_description.as_str()); assert!(prompt.chars().count() <= PUZZLE_TEXT_TO_IMAGE_PROMPT_MAX_CHARS); - assert!(prompt.contains("1:1")); + assert!(prompt.contains("输出画面比例为1:1")); assert!(prompt.contains("主体要清晰集中")); assert!(prompt.contains("避免文字、水印、边框和 UI 元素")); } diff --git a/server-rs/crates/api-server/src/puzzle.rs b/server-rs/crates/api-server/src/puzzle.rs index 018c02c7..84dfac4f 100644 --- a/server-rs/crates/api-server/src/puzzle.rs +++ b/server-rs/crates/api-server/src/puzzle.rs @@ -1,6 +1,5 @@ use std::{ collections::BTreeMap, - error::Error as StdError, time::{Duration, Instant, SystemTime, UNIX_EPOCH}, }; @@ -21,10 +20,8 @@ use module_assets::{ }; use module_puzzle::{PuzzleGeneratedImageCandidate, PuzzleRuntimeLevelStatus}; use platform_llm::{LlmMessage, LlmMessageContentPart, LlmTextRequest}; -use platform_oss::{ - LegacyAssetPrefix, OssHeadObjectRequest, OssObjectAccess, OssPutObjectRequest, - OssSignedGetObjectUrlRequest, -}; +use platform_oss::{LegacyAssetPrefix, OssSignedGetObjectUrlRequest}; +use platform_oss::{OssHeadObjectRequest, OssObjectAccess, OssPutObjectRequest}; use serde_json::{Map, Value, json}; use shared_contracts::{ creation_audio::CreationAudioAsset, @@ -80,10 +77,11 @@ use crate::{ should_skip_asset_operation_billing_for_connectivity, }, auth::AuthenticatedAccessToken, + generated_asset_sheets::apply_generated_asset_sheet_green_screen_alpha, http_error::AppError, llm_model_routing::{CREATION_TEMPLATE_LLM_MODEL, PUZZLE_LEVEL_NAME_VISION_LLM_MODEL}, openai_image_generation::{ - DownloadedOpenAiImage, VECTOR_ENGINE_GPT_IMAGE_2_MODEL, build_openai_image_http_client, + DownloadedOpenAiImage, GPT_IMAGE_2_MODEL, build_openai_image_http_client, create_openai_image_generation, require_openai_image_settings, }, platform_errors::map_oss_error, @@ -105,12 +103,9 @@ use crate::{ }, puzzle_gallery_cache::{build_puzzle_gallery_window_response, puzzle_gallery_cached_json}, request_context::RequestContext, - state::AppState, - vector_engine_audio_generation::{ - GeneratedCreationAudioTarget, generate_background_music_asset_for_creation, - }, - work_author::resolve_work_author_by_user_id, - work_play_tracking::{WorkPlayTrackingDraft, record_work_play_start_after_success}, + state::PuzzleApiState, + work_author::resolve_puzzle_work_author_by_user_id, + work_play_tracking::{WorkPlayTrackingDraft, record_puzzle_work_play_start_after_success}, }; const PUZZLE_AGENT_API_BASE_PROVIDER: &str = "puzzle-agent"; @@ -121,8 +116,6 @@ const PUZZLE_IMAGE_MODEL_GPT_IMAGE_2: &str = "gpt-image-2"; const PUZZLE_IMAGE_MODEL_GEMINI_31_FLASH_PREVIEW: &str = "gemini-3.1-flash-image-preview"; const PUZZLE_IMAGE_GENERATION_POINTS_COST: u64 = 2; const PUZZLE_ENTITY_KIND: &str = "puzzle_work"; -const PUZZLE_BACKGROUND_MUSIC_ASSET_KIND: &str = "puzzle_background_music"; -const PUZZLE_BACKGROUND_MUSIC_SLOT: &str = "background_music"; #[cfg(test)] const PUZZLE_GENERATED_IMAGE_SIZE: &str = "1024*1024"; const PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE: &str = "1024x1024"; @@ -131,9 +124,13 @@ const PUZZLE_LEVEL_NAME_VISION_IMAGE_MAX_SIDE: u32 = 768; const PUZZLE_LEVEL_NAME_VISION_MAX_TOKENS: u32 = 512; const PUZZLE_REFERENCE_IMAGE_MAX_BYTES: usize = 8 * 1024 * 1024; const PUZZLE_REFERENCE_IMAGE_SOURCE_LIMIT: usize = 5; -const PUZZLE_VECTOR_ENGINE_IMAGE_EDIT_MODEL: &str = "gpt-image-2"; const PUZZLE_UI_BACKGROUND_PROMPT_FALLBACK_MARKER: &str = "移动端拼图游戏纯背景,题材氛围清晰,不包含拼图槽或 UI 元素"; +const PUZZLE_VECTOR_ENGINE_SQUARE_IMAGE_SIZE: &str = "1024x1024"; +const PUZZLE_VECTOR_ENGINE_PORTRAIT_IMAGE_SIZE: &str = "1024x1536"; +const PUZZLE_LEVEL_SCENE_IMAGE_PROMPT: &str = "参考图作为拼图画面,生成对应的拼图游戏关卡画面,要求画面中所有元素精致且风格高度一致,画面中所有UI细节饱满精致、完成度高、顶级游戏品质\n\n画面元素:\n返回按钮位于顶部左上角,顶部中间显示关卡标题“第1关 影”和倒计时时间,右上角显示设置按钮\n画面中间是一个正方形的3*3拼图,拼图区域宽度与画面宽度同宽,紧贴画面横向边缘,拼图区域边界带有边框装饰\n拼图区域下方包含一个下一关按钮,仅在关卡完成时显示\n底部是三个贴合画面主题的道具按钮分别为“提示”、“原图”、“冻结”\n道具按钮上不要显示次数标注,返回按钮和设置按钮旁禁止标注文字"; +const PUZZLE_UI_SPRITESHEET_IMAGE_PROMPT: &str = "提取画面中的UI元素,将返回按钮、设置按钮、下一关按钮、提示按钮、原图按钮、冻结按钮整理成纯绿色绿幕背景的spritesheet。背景必须是统一纯绿色绿幕(高饱和亮绿色,接近 #00FF00),背景平整无纹理、无渐变、无阴影、无场景内容,后端会在生图后将绿幕扣成透明并把透明背景 PNG 存到 OSS。按钮顺序必须按原图位置从左到右、从上到下排列:返回、设置、下一关、提示、原图、冻结。按钮素材内必须保留对应中文文字,每个按钮必须是独立完整图形,按钮之间保留足够纯绿色绿幕空白,不能相互接触、重叠或连成一片,方便运行态按自动边界检测识别矩形素材。返回按钮和设置按钮不要额外画白色外圈、白底圆环或浮雕外框,直接画扁平图标本体。按钮自身不得使用接近 #00FF00 的高饱和纯绿;绿色题材只能使用深绿、橄榄绿、金绿或蓝绿,并用清晰描边与绿幕区分。禁止水印、数字、次数标注、透明背景、背景图、拼图块、棋盘、网格线、按钮外标签和额外按钮。"; +const PUZZLE_LEVEL_BACKGROUND_IMAGE_PROMPT: &str = "移除参考图中所有UI元素、移除拼图画面,仅保留背景图,补全被覆盖的背景图内容。禁止在背景中出现人像或和拼图画面中主体一致的内容"; mod handlers; pub(crate) use self::handlers::*; diff --git a/server-rs/crates/api-server/src/puzzle/draft.rs b/server-rs/crates/api-server/src/puzzle/draft.rs index d301c0b5..d639f8f2 100644 --- a/server-rs/crates/api-server/src/puzzle/draft.rs +++ b/server-rs/crates/api-server/src/puzzle/draft.rs @@ -24,7 +24,7 @@ pub(crate) fn build_puzzle_form_seed_text_from_parts( } pub(crate) async fn save_puzzle_form_payload_before_compile( - state: &AppState, + state: &PuzzleApiState, request_context: &RequestContext, session_id: &str, owner_user_id: &str, @@ -76,7 +76,7 @@ pub(crate) async fn save_puzzle_form_payload_before_compile( } pub(crate) async fn create_seeded_puzzle_session_when_form_save_missing( - state: &AppState, + state: &PuzzleApiState, request_context: &RequestContext, session_id: &str, owner_user_id: &str, @@ -184,6 +184,12 @@ pub(crate) fn parse_puzzle_level_records_from_module_json( ui_background_prompt: level.ui_background_prompt, ui_background_image_src: level.ui_background_image_src, ui_background_image_object_key: level.ui_background_image_object_key, + level_scene_image_src: level.level_scene_image_src, + level_scene_image_object_key: level.level_scene_image_object_key, + ui_spritesheet_image_src: level.ui_spritesheet_image_src, + ui_spritesheet_image_object_key: level.ui_spritesheet_image_object_key, + level_background_image_src: level.level_background_image_src, + level_background_image_object_key: level.level_background_image_object_key, background_music: level .background_music .map(map_puzzle_audio_asset_domain_record), @@ -209,7 +215,7 @@ pub(crate) fn parse_puzzle_level_records_from_module_json( } pub(crate) async fn get_puzzle_session_for_image_generation( - state: &AppState, + state: &PuzzleApiState, session_id: String, owner_user_id: String, payload: &ExecutePuzzleAgentActionRequest, @@ -357,6 +363,12 @@ pub(crate) fn serialize_puzzle_levels_response( "ui_background_prompt": level.ui_background_prompt, "ui_background_image_src": level.ui_background_image_src, "ui_background_image_object_key": level.ui_background_image_object_key, + "level_scene_image_src": level.level_scene_image_src, + "level_scene_image_object_key": level.level_scene_image_object_key, + "ui_spritesheet_image_src": level.ui_spritesheet_image_src, + "ui_spritesheet_image_object_key": level.ui_spritesheet_image_object_key, + "level_background_image_src": level.level_background_image_src, + "level_background_image_object_key": level.level_background_image_object_key, "background_music": puzzle_audio_asset_response_module_json(&level.background_music), "candidates": level .candidates @@ -411,6 +423,12 @@ pub(crate) fn normalize_puzzle_levels_json_for_module( "ui_background_prompt": level.ui_background_prompt, "ui_background_image_src": level.ui_background_image_src, "ui_background_image_object_key": level.ui_background_image_object_key, + "level_scene_image_src": level.level_scene_image_src, + "level_scene_image_object_key": level.level_scene_image_object_key, + "ui_spritesheet_image_src": level.ui_spritesheet_image_src, + "ui_spritesheet_image_object_key": level.ui_spritesheet_image_object_key, + "level_background_image_src": level.level_background_image_src, + "level_background_image_object_key": level.level_background_image_object_key, "background_music": puzzle_audio_asset_response_module_json(&level.background_music), "candidates": level .candidates @@ -469,7 +487,7 @@ impl PuzzleLevelNaming { } pub(crate) async fn generate_puzzle_first_level_name( - state: &AppState, + state: &PuzzleApiState, picture_description: &str, ) -> PuzzleLevelNaming { if let Some(llm_client) = state.llm_client() { @@ -511,7 +529,7 @@ pub(crate) async fn generate_puzzle_first_level_name( } pub(crate) async fn generate_puzzle_first_level_name_from_image( - state: &AppState, + state: &PuzzleApiState, picture_description: &str, image: &PuzzleDownloadedImage, ) -> Option { @@ -918,6 +936,15 @@ pub(crate) fn build_puzzle_levels_with_primary_update( levels[index].ui_background_image_src = target_level.ui_background_image_src.clone(); levels[index].ui_background_image_object_key = target_level.ui_background_image_object_key.clone(); + levels[index].level_scene_image_src = target_level.level_scene_image_src.clone(); + levels[index].level_scene_image_object_key = + target_level.level_scene_image_object_key.clone(); + levels[index].ui_spritesheet_image_src = target_level.ui_spritesheet_image_src.clone(); + levels[index].ui_spritesheet_image_object_key = + target_level.ui_spritesheet_image_object_key.clone(); + levels[index].level_background_image_src = target_level.level_background_image_src.clone(); + levels[index].level_background_image_object_key = + target_level.level_background_image_object_key.clone(); if let Some(picture_reference) = picture_reference .map(str::trim) .filter(|value| !value.is_empty()) @@ -1033,42 +1060,31 @@ pub(crate) fn attach_puzzle_level_ui_background( levels[index].ui_background_image_object_key = Some(generated.object_key); } -pub(crate) async fn generate_puzzle_background_music_required( - state: &AppState, - owner_user_id: &str, - profile_id: &str, - title: &str, -) -> Result { - let normalized_title = title.trim(); - if normalized_title.is_empty() { - return Err( - AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ - "provider": PUZZLE_AGENT_API_BASE_PROVIDER, - "message": "拼图草稿背景音乐名称为空,无法完成背景音乐生成", - })), - ); - } - generate_background_music_asset_for_creation( - state, - owner_user_id, - String::new(), - normalized_title.to_string(), - Some("轻快, 拼图, 循环, instrumental".to_string()), - None, - GeneratedCreationAudioTarget { - entity_kind: PUZZLE_ENTITY_KIND.to_string(), - entity_id: profile_id.to_string(), - slot: PUZZLE_BACKGROUND_MUSIC_SLOT.to_string(), - asset_kind: PUZZLE_BACKGROUND_MUSIC_ASSET_KIND.to_string(), - profile_id: Some(profile_id.to_string()), - storage_prefix: LegacyAssetPrefix::PuzzleAssets, - }, - ) - .await +pub(crate) fn attach_puzzle_level_asset_bundle( + levels: &mut [PuzzleDraftLevelRecord], + level_id: &str, + generated: GeneratedPuzzleLevelAssetBundle, +) { + let Some(index) = levels + .iter() + .position(|level| level.level_id == level_id) + .or_else(|| (!levels.is_empty()).then_some(0)) + else { + return; + }; + let level = &mut levels[index]; + level.level_scene_image_src = Some(generated.level_scene.image_src); + level.level_scene_image_object_key = Some(generated.level_scene.object_key); + level.ui_spritesheet_image_src = Some(generated.ui_spritesheet.image_src); + level.ui_spritesheet_image_object_key = Some(generated.ui_spritesheet.object_key); + level.level_background_image_src = Some(generated.level_background.image_src.clone()); + level.level_background_image_object_key = Some(generated.level_background.object_key.clone()); + level.ui_background_image_src = Some(generated.level_background.image_src); + level.ui_background_image_object_key = Some(generated.level_background.object_key); } pub(crate) async fn generate_puzzle_initial_ui_background_required( - state: &AppState, + state: &PuzzleApiState, owner_user_id: &str, session_id: &str, draft: &PuzzleResultDraftRecord, @@ -1086,26 +1102,56 @@ pub(crate) async fn generate_puzzle_initial_ui_background_required( Ok((prompt, generated)) } +pub(crate) async fn generate_puzzle_level_asset_bundle_required( + state: &PuzzleApiState, + owner_user_id: &str, + session_id: &str, + target_level: &PuzzleDraftLevelRecord, + puzzle_image: &PuzzleDownloadedImage, +) -> Result { + generate_puzzle_level_asset_bundle( + state, + owner_user_id, + session_id, + target_level.level_name.as_str(), + puzzle_image, + ) + .await +} + pub(crate) fn ensure_puzzle_initial_level_assets_ready( level: &PuzzleDraftLevelRecord, ) -> Result<(), AppError> { - let has_ui_background = level - .ui_background_image_src + let has_level_background = level + .level_background_image_src .as_deref() .map(str::trim) .is_some_and(|value| !value.is_empty()) || level - .ui_background_image_object_key + .level_background_image_object_key .as_deref() .map(str::trim) .is_some_and(|value| !value.is_empty()); - if has_ui_background { + let has_ui_spritesheet = level + .ui_spritesheet_image_src + .as_deref() + .map(str::trim) + .is_some_and(|value| !value.is_empty()) + || level + .ui_spritesheet_image_object_key + .as_deref() + .map(str::trim) + .is_some_and(|value| !value.is_empty()); + if has_level_background && has_ui_spritesheet { return Ok(()); } let mut missing = Vec::new(); - if !has_ui_background { - missing.push("UI背景图"); + if !has_level_background { + missing.push("关卡背景图"); + } + if !has_ui_spritesheet { + missing.push("UI spritesheet"); } Err( @@ -1128,7 +1174,7 @@ pub(crate) fn find_puzzle_level_for_initial_asset_check<'a>( } pub(crate) async fn compile_puzzle_draft_with_initial_cover( - state: &AppState, + state: &PuzzleApiState, session_id: String, owner_user_id: String, prompt_text: Option<&str>, @@ -1159,8 +1205,8 @@ pub(crate) async fn compile_puzzle_draft_with_initial_cover( target_level.level_name = generated_naming.level_name.clone(); target_level.ui_background_prompt = generated_naming.ui_background_prompt.clone(); let mut generated_metadata = generated_naming; - // 点击生成草稿时一次性完成首图生成、UI 背景生成与正式图选定,前端只展示进度,不再承担业务编排。 - let candidates_future = generate_puzzle_image_candidates( + // 点击生成草稿时一次性完成拼图主图和运行态资产包,前端只展示进度,不再承担业务编排。 + let mut candidates = generate_puzzle_image_candidates( state, owner_user_id.as_str(), &compiled_session.session_id, @@ -1171,18 +1217,8 @@ pub(crate) async fn compile_puzzle_draft_with_initial_cover( image_model, 1, target_level.candidates.len(), - ); - let ui_background_future = generate_puzzle_initial_ui_background_required( - state, - owner_user_id.as_str(), - compiled_session.session_id.as_str(), - &draft, - &target_level, - ); - // 中文注释:命名稳定后并行发起首关图与 UI 背景,避免两次外部生图串行等待。 - let (candidates_result, ui_background_result) = - tokio::join!(candidates_future, ui_background_future); - let mut candidates = candidates_result?; + ) + .await?; if let Some(first_candidate) = candidates.first() && let Some(refined_naming) = generate_puzzle_first_level_name_from_image( state, @@ -1218,19 +1254,25 @@ pub(crate) async fn compile_puzzle_draft_with_initial_cover( "message": "拼图候选图生成结果为空", })) })?; - // 中文注释:拼图草稿音频生成临时关闭,首版生成只补首图与 UI 背景。 - let (ui_prompt, ui_background) = ui_background_result?; - attach_puzzle_level_ui_background( - &mut updated_levels, - target_level.level_id.as_str(), - ui_prompt, - ui_background, - ); + // 中文注释:拼图草稿音频生成临时关闭,首版生成只补首图、关卡背景和 UI spritesheet。 if let Some(selected_candidate) = candidates .iter() .find(|candidate| candidate.record.selected) .or_else(|| candidates.first()) { + let asset_bundle = generate_puzzle_level_asset_bundle_required( + state, + owner_user_id.as_str(), + compiled_session.session_id.as_str(), + &target_level, + &selected_candidate.downloaded_image, + ) + .await?; + attach_puzzle_level_asset_bundle( + &mut updated_levels, + target_level.level_id.as_str(), + asset_bundle, + ); attach_selected_puzzle_candidate_to_levels( &mut updated_levels, target_level.level_id.as_str(), @@ -1398,7 +1440,7 @@ pub(crate) async fn compile_puzzle_draft_with_initial_cover( } pub(crate) async fn compile_puzzle_draft_with_uploaded_cover( - state: &AppState, + state: &PuzzleApiState, session_id: String, owner_user_id: String, prompt_text: Option<&str>, @@ -1417,7 +1459,12 @@ pub(crate) async fn compile_puzzle_draft_with_uploaded_cover( })?; let http_client = reqwest::Client::new(); let uploaded_downloaded_image = - resolve_puzzle_reference_image_as_data_url(state, &http_client, uploaded_image_src) + resolve_puzzle_reference_image( + state, + &http_client, + uploaded_image_src, + Some(owner_user_id.as_str()), + ) .await .map(PuzzleDownloadedImage::from_resolved_reference_image) .map_err(|error| { @@ -1425,7 +1472,7 @@ pub(crate) async fn compile_puzzle_draft_with_uploaded_cover( AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": PUZZLE_AGENT_API_BASE_PROVIDER, "field": "referenceImageSrc", - "message": "关闭 AI 重绘时上传图必须是图片 Data URL 或历史生成图片路径。", + "message": "关闭 AI 重绘时上传图必须是拼图图片 assetObjectId、图片 Data URL 或历史生成图片路径。", })) } else { error @@ -1484,7 +1531,7 @@ pub(crate) async fn compile_puzzle_draft_with_uploaded_cover( let generated_level_name = target_level.level_name.clone(); let mut updated_levels = build_puzzle_levels_with_primary_update(&draft, &target_level, reference_image_src); - let persist_upload_future = persist_puzzle_generated_asset( + let persisted_upload = persist_puzzle_generated_asset( state, owner_user_id.as_str(), &compiled_session.session_id, @@ -1493,24 +1540,20 @@ pub(crate) async fn compile_puzzle_draft_with_uploaded_cover( "uploaded-direct", uploaded_downloaded_image.clone(), current_utc_micros(), - ); - let ui_background_future = generate_puzzle_initial_ui_background_required( + ) + .await?; + let asset_bundle = generate_puzzle_level_asset_bundle_required( state, owner_user_id.as_str(), compiled_session.session_id.as_str(), - &draft, &target_level, - ); - // 中文注释:直用上传图时并行完成上传图持久化与 UI 背景生成;音频生成入口临时关闭。 - let (persisted_upload_result, ui_background_result) = - tokio::join!(persist_upload_future, ui_background_future); - let persisted_upload = persisted_upload_result?; - let (ui_prompt, ui_background) = ui_background_result?; - attach_puzzle_level_ui_background( + &uploaded_downloaded_image, + ) + .await?; + attach_puzzle_level_asset_bundle( &mut updated_levels, target_level.level_id.as_str(), - ui_prompt, - ui_background, + asset_bundle, ); attach_selected_puzzle_candidate_to_levels( &mut updated_levels, diff --git a/server-rs/crates/api-server/src/puzzle/generation.rs b/server-rs/crates/api-server/src/puzzle/generation.rs index 5055d0c3..5a3d9a2a 100644 --- a/server-rs/crates/api-server/src/puzzle/generation.rs +++ b/server-rs/crates/api-server/src/puzzle/generation.rs @@ -12,19 +12,77 @@ pub(crate) fn map_puzzle_generation_endpoint_error(error: AppError) -> AppError error } -pub(crate) fn should_fallback_puzzle_reference_edit_to_generation(error: &AppError) -> bool { - error.status_code() == StatusCode::GATEWAY_TIMEOUT - || is_puzzle_request_timeout_message(error.body_text().as_str()) +pub(crate) fn should_use_uploaded_puzzle_image_directly( + reference_image_src: Option<&str>, + ai_redraw: bool, +) -> bool { + !ai_redraw + && reference_image_src + .map(str::trim) + .is_some_and(|value| !value.is_empty()) +} + +pub(crate) async fn create_uploaded_puzzle_image_candidate( + state: &PuzzleApiState, + owner_user_id: &str, + session_id: &str, + level_name: &str, + prompt: &str, + reference_image_src: &str, + candidate_start_index: usize, +) -> Result { + let http_client = reqwest::Client::new(); + let downloaded_image = + resolve_puzzle_reference_image_as_data_url(state, &http_client, reference_image_src) + .await + .map(PuzzleDownloadedImage::from_resolved_reference_image) + .map_err(|error| { + if error.status_code() == StatusCode::BAD_REQUEST { + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": PUZZLE_AGENT_API_BASE_PROVIDER, + "field": "referenceImageSrc", + "message": "关闭 AI 重绘时上传图必须是图片 Data URL 或历史生成图片路径。", + })) + } else { + error + } + })?; + let candidate_id = format!("{session_id}-candidate-{}", candidate_start_index + 1); + let asset = persist_puzzle_generated_asset( + state, + owner_user_id, + session_id, + level_name, + candidate_id.as_str(), + "uploaded-direct", + downloaded_image.clone(), + current_utc_micros(), + ) + .await + .map_err(map_puzzle_generation_endpoint_error)?; + + Ok(GeneratedPuzzleImageCandidate { + record: PuzzleGeneratedImageCandidateRecord { + candidate_id, + image_src: asset.image_src, + asset_id: asset.asset_id, + prompt: prompt.to_string(), + actual_prompt: None, + source_type: "uploaded".to_string(), + selected: true, + }, + downloaded_image, + }) } pub(crate) async fn generate_puzzle_image_candidates( - state: &AppState, + state: &PuzzleApiState, owner_user_id: &str, session_id: &str, level_name: &str, prompt: &str, reference_image_src: Option<&str>, - use_reference_image_edit: bool, + use_reference_image_generation: bool, image_model: Option<&str>, candidate_count: u32, candidate_start_index: usize, @@ -34,11 +92,13 @@ pub(crate) async fn generate_puzzle_image_candidates( let resolved_model = resolve_puzzle_image_model(image_model); let http_client = build_puzzle_image_http_client(state, resolved_model)?; let has_reference_image = has_puzzle_reference_image(reference_image_src); - let should_use_reference_image_edit = - should_use_puzzle_reference_image_edit(reference_image_src, use_reference_image_edit); + let should_use_reference_image_generation = should_use_puzzle_reference_image_generation( + reference_image_src, + use_reference_image_generation, + ); let actual_prompt = build_puzzle_vector_engine_generation_prompt( build_puzzle_image_prompt(level_name, prompt).as_str(), - should_use_reference_image_edit, + should_use_reference_image_generation, ); tracing::info!( provider = resolved_model.provider_name(), @@ -48,18 +108,19 @@ pub(crate) async fn generate_puzzle_image_candidates( prompt_chars = prompt.chars().count(), actual_prompt_chars = actual_prompt.chars().count(), has_reference_image, - use_reference_image_edit = should_use_reference_image_edit, + use_reference_image_generation = should_use_reference_image_generation, "拼图图片生成请求已准备" ); let reference_image_started_at = Instant::now(); let reference_image = match reference_image_src .map(str::trim) .filter(|value| !value.is_empty()) - .filter(|_| should_use_reference_image_edit) + .filter(|_| should_use_reference_image_generation) { Some(source) => { let resolved = - resolve_puzzle_reference_image_as_data_url(state, &http_client, source).await?; + resolve_puzzle_reference_image(state, &http_client, source, Some(owner_user_id)) + .await?; tracing::info!( provider = resolved_model.provider_name(), image_model = resolved_model.request_model_name(), @@ -74,14 +135,14 @@ pub(crate) async fn generate_puzzle_image_candidates( } None => None, }; - if !should_use_reference_image_edit { + if !should_use_reference_image_generation { tracing::info!( provider = resolved_model.provider_name(), image_model = resolved_model.request_model_name(), session_id, level_name, has_reference_image, - use_reference_image_edit = should_use_reference_image_edit, + use_reference_image_generation = should_use_reference_image_generation, elapsed_ms = reference_image_started_at.elapsed().as_millis() as u64, "拼图参考图解析跳过" ); @@ -90,7 +151,7 @@ pub(crate) async fn generate_puzzle_image_candidates( // 中文注释:拼图作品资产统一按 1:1 正方形生成,前端运行时也按正方形棋盘切块承载。 let settings = require_puzzle_vector_engine_settings(state)?; let vector_engine_started_at = Instant::now(); - let generated = if should_use_reference_image_edit { + let generated = if should_use_reference_image_generation { let reference_image = reference_image.as_ref().ok_or_else(|| { AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "puzzle", @@ -98,43 +159,17 @@ pub(crate) async fn generate_puzzle_image_candidates( "message": "AI 重绘需要提供参考图。", })) })?; - let edit_result = create_puzzle_vector_engine_image_edit( + create_puzzle_vector_engine_image_generation( &http_client, &settings, + resolved_model, actual_prompt.as_str(), PUZZLE_DEFAULT_NEGATIVE_PROMPT, PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE, count, - reference_image, + Some(reference_image), ) - .await; - match edit_result { - Ok(generated) => Ok(generated), - Err(error) if should_fallback_puzzle_reference_edit_to_generation(&error) => { - tracing::warn!( - provider = resolved_model.provider_name(), - image_model = resolved_model.request_model_name(), - session_id, - level_name, - reference_mime = %reference_image.mime_type, - reference_bytes = reference_image.bytes_len, - error = %error, - "拼图参考图编辑接口超时,降级为带参考图的生成接口" - ); - create_puzzle_vector_engine_image_generation( - &http_client, - &settings, - resolved_model, - actual_prompt.as_str(), - PUZZLE_DEFAULT_NEGATIVE_PROMPT, - PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE, - count, - Some(reference_image), - ) - .await - } - Err(error) => Err(error), - } + .await } else { create_puzzle_vector_engine_image_generation( &http_client, @@ -219,13 +254,13 @@ pub(crate) async fn generate_puzzle_image_candidates( } pub(crate) async fn generate_puzzle_ui_background_image( - state: &AppState, + state: &PuzzleApiState, owner_user_id: &str, session_id: &str, level_name: &str, prompt: &str, ) -> Result { - let settings = require_openai_image_settings(state)?; + let settings = require_openai_image_settings(state.root_state())?; let http_client = build_openai_image_http_client(&settings)?; let generated = create_openai_image_generation( &http_client, @@ -255,6 +290,175 @@ pub(crate) async fn generate_puzzle_ui_background_image( .await } +pub(crate) async fn generate_puzzle_level_asset_bundle( + state: &PuzzleApiState, + owner_user_id: &str, + session_id: &str, + level_name: &str, + puzzle_image: &PuzzleDownloadedImage, +) -> Result { + let settings = require_puzzle_vector_engine_settings(state)?; + let http_client = build_puzzle_image_http_client(state, PuzzleImageModel::GptImage2)?; + let puzzle_reference = build_puzzle_downloaded_image_reference(puzzle_image); + let scene_generated = create_puzzle_vector_engine_image_generation( + &http_client, + &settings, + PuzzleImageModel::GptImage2, + PUZZLE_LEVEL_SCENE_IMAGE_PROMPT, + "", + PUZZLE_VECTOR_ENGINE_PORTRAIT_IMAGE_SIZE, + 1, + Some(&puzzle_reference), + ) + .await + .map_err(map_puzzle_generation_endpoint_error)?; + let scene_image = scene_generated.images.into_iter().next().ok_or_else(|| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": VECTOR_ENGINE_PROVIDER, + "message": "拼图关卡画面图生成失败:未返回图片", + })) + })?; + let scene_reference = build_puzzle_downloaded_image_reference(&scene_image); + let scene_persist_future = persist_puzzle_level_asset_image( + state, + owner_user_id, + session_id, + level_name, + scene_generated.task_id.as_str(), + "level-scene", + "puzzle_level_scene_image", + "level_scene", + "scene", + scene_image, + ); + let spritesheet_future = generate_and_persist_puzzle_level_asset( + state, + &http_client, + &settings, + owner_user_id, + session_id, + level_name, + PUZZLE_UI_SPRITESHEET_IMAGE_PROMPT, + PUZZLE_VECTOR_ENGINE_SQUARE_IMAGE_SIZE, + &scene_reference, + "ui-spritesheet", + "puzzle_ui_spritesheet_image", + "ui_spritesheet", + "spritesheet", + ); + let background_future = generate_and_persist_puzzle_level_asset( + state, + &http_client, + &settings, + owner_user_id, + session_id, + level_name, + PUZZLE_LEVEL_BACKGROUND_IMAGE_PROMPT, + PUZZLE_VECTOR_ENGINE_PORTRAIT_IMAGE_SIZE, + &scene_reference, + "level-background", + "puzzle_level_background_image", + "level_background", + "background", + ); + let (level_scene, ui_spritesheet, level_background) = + tokio::join!(scene_persist_future, spritesheet_future, background_future); + + Ok(GeneratedPuzzleLevelAssetBundle { + level_scene: level_scene?, + ui_spritesheet: ui_spritesheet?, + level_background: level_background?, + }) +} + +async fn generate_and_persist_puzzle_level_asset( + state: &PuzzleApiState, + http_client: &reqwest::Client, + settings: &PuzzleVectorEngineSettings, + owner_user_id: &str, + session_id: &str, + level_name: &str, + prompt: &str, + size: &str, + reference_image: &PuzzleResolvedReferenceImage, + path_segment: &str, + asset_kind: &str, + slot: &str, + file_stem: &str, +) -> Result { + let generated = create_puzzle_vector_engine_image_generation( + http_client, + settings, + PuzzleImageModel::GptImage2, + prompt, + "", + size, + 1, + Some(reference_image), + ) + .await + .map_err(map_puzzle_generation_endpoint_error)?; + let image = generated.images.into_iter().next().ok_or_else(|| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": VECTOR_ENGINE_PROVIDER, + "message": format!("拼图关卡资产生成失败:{asset_kind} 未返回图片"), + })) + })?; + let image = if slot == "ui_spritesheet" { + make_puzzle_ui_spritesheet_image_transparent(image)? + } else { + image + }; + + persist_puzzle_level_asset_image( + state, + owner_user_id, + session_id, + level_name, + generated.task_id.as_str(), + path_segment, + asset_kind, + slot, + file_stem, + image, + ) + .await +} + +pub(crate) fn make_puzzle_ui_spritesheet_image_transparent( + image: PuzzleDownloadedImage, +) -> Result { + let source = image::load_from_memory(image.bytes.as_slice()).map_err(|error| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": VECTOR_ENGINE_PROVIDER, + "message": format!("拼图 UI spritesheet 图解码失败:{error}"), + })) + })?; + + let mut encoded = std::io::Cursor::new(Vec::new()); + apply_generated_asset_sheet_green_screen_alpha(source) + .write_to(&mut encoded, ImageFormat::Png) + .map_err(|error| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": VECTOR_ENGINE_PROVIDER, + "message": format!("拼图 UI spritesheet 图透明化失败:{error}"), + })) + })?; + + Ok(PuzzleDownloadedImage { + extension: "png".to_string(), + mime_type: "image/png".to_string(), + bytes: encoded.into_inner(), + }) +} + +#[cfg(test)] +pub(crate) fn make_puzzle_ui_spritesheet_image_transparent_for_test( + image: PuzzleDownloadedImage, +) -> Result { + make_puzzle_ui_spritesheet_image_transparent(image) +} + #[cfg(test)] pub(crate) fn build_puzzle_ui_background_request_prompt_for_test( level_name: &str, @@ -262,3 +466,45 @@ pub(crate) fn build_puzzle_ui_background_request_prompt_for_test( ) -> String { build_puzzle_ui_background_generation_prompt(level_name, prompt) } + +#[cfg(test)] +pub(crate) fn build_puzzle_level_scene_image_request_body_for_test( + reference_image: &PuzzleDownloadedImage, +) -> Result { + Ok(build_puzzle_vector_engine_image_request_body( + PuzzleImageModel::GptImage2, + PUZZLE_LEVEL_SCENE_IMAGE_PROMPT, + "", + PUZZLE_VECTOR_ENGINE_PORTRAIT_IMAGE_SIZE, + 1, + Some(&build_puzzle_downloaded_image_reference(reference_image)), + )) +} + +#[cfg(test)] +pub(crate) fn build_puzzle_ui_spritesheet_request_body_for_test( + reference_image: &PuzzleDownloadedImage, +) -> Result { + Ok(build_puzzle_vector_engine_image_request_body( + PuzzleImageModel::GptImage2, + PUZZLE_UI_SPRITESHEET_IMAGE_PROMPT, + "", + PUZZLE_VECTOR_ENGINE_SQUARE_IMAGE_SIZE, + 1, + Some(&build_puzzle_downloaded_image_reference(reference_image)), + )) +} + +#[cfg(test)] +pub(crate) fn build_puzzle_level_background_request_body_for_test( + reference_image: &PuzzleDownloadedImage, +) -> Result { + Ok(build_puzzle_vector_engine_image_request_body( + PuzzleImageModel::GptImage2, + PUZZLE_LEVEL_BACKGROUND_IMAGE_PROMPT, + "", + PUZZLE_VECTOR_ENGINE_PORTRAIT_IMAGE_SIZE, + 1, + Some(&build_puzzle_downloaded_image_reference(reference_image)), + )) +} diff --git a/server-rs/crates/api-server/src/puzzle/handlers.rs b/server-rs/crates/api-server/src/puzzle/handlers.rs index a47c00b1..63be5836 100644 --- a/server-rs/crates/api-server/src/puzzle/handlers.rs +++ b/server-rs/crates/api-server/src/puzzle/handlers.rs @@ -1,7 +1,7 @@ use super::*; pub async fn create_puzzle_agent_session( - State(state): State, + State(state): State, Extension(request_context): Extension, Extension(authenticated): Extension, payload: Result, JsonRejection>, @@ -46,7 +46,7 @@ pub async fn create_puzzle_agent_session( } pub async fn generate_puzzle_onboarding_work( - State(state): State, + State(state): State, Extension(request_context): Extension, payload: Result, JsonRejection>, ) -> Result, Response> { @@ -113,6 +113,12 @@ pub async fn generate_puzzle_onboarding_work( ui_background_prompt: naming.ui_background_prompt.clone(), ui_background_image_src: None, ui_background_image_object_key: None, + level_scene_image_src: None, + level_scene_image_object_key: None, + ui_spritesheet_image_src: None, + ui_spritesheet_image_object_key: None, + level_background_image_src: None, + level_background_image_object_key: None, background_music: None, candidates, selected_candidate_id: Some(selected.candidate_id.clone()), @@ -161,7 +167,7 @@ pub async fn generate_puzzle_onboarding_work( } pub async fn save_puzzle_onboarding_work( - State(state): State, + State(state): State, Extension(request_context): Extension, Extension(authenticated): Extension, payload: Result, JsonRejection>, @@ -270,7 +276,7 @@ pub async fn save_puzzle_onboarding_work( } pub async fn get_puzzle_agent_session( - State(state): State, + State(state): State, AxumPath(session_id): AxumPath, Extension(request_context): Extension, Extension(authenticated): Extension, @@ -303,7 +309,7 @@ pub async fn get_puzzle_agent_session( } pub async fn submit_puzzle_agent_message( - State(state): State, + State(state): State, AxumPath(session_id): AxumPath, Extension(request_context): Extension, Extension(authenticated): Extension, @@ -359,7 +365,7 @@ pub async fn submit_puzzle_agent_message( llm_client: state.llm_client(), session: &submitted_session, quick_fill_requested: payload.quick_fill_requested.unwrap_or(false), - enable_web_search: state.config.creation_agent_llm_web_search_enabled, + enable_web_search: state.creation_agent_llm_web_search_enabled(), }, |_| {}, ) @@ -401,7 +407,7 @@ pub async fn submit_puzzle_agent_message( } pub async fn stream_puzzle_agent_message( - State(state): State, + State(state): State, AxumPath(session_id): AxumPath, Extension(request_context): Extension, Extension(authenticated): Extension, @@ -464,7 +470,7 @@ pub async fn stream_puzzle_agent_message( llm_client: state.llm_client(), session: &session, quick_fill_requested, - enable_web_search: state.config.creation_agent_llm_web_search_enabled, + enable_web_search: state.creation_agent_llm_web_search_enabled(), }, move |text| { let _ = reply_tx.send(text.to_string()); @@ -554,7 +560,7 @@ pub async fn stream_puzzle_agent_message( } pub async fn execute_puzzle_agent_action( - State(state): State, + State(state): State, AxumPath(session_id): AxumPath, Extension(request_context): Extension, Extension(authenticated): Extension, @@ -595,6 +601,8 @@ pub async fn execute_puzzle_agent_action( has_reference_image = has_puzzle_reference_images( payload.reference_image_src.as_deref(), payload.reference_image_srcs.as_slice(), + payload.reference_image_asset_object_id.as_deref(), + payload.reference_image_asset_object_ids.as_slice(), ), "拼图 Agent action 开始执行" ); @@ -604,6 +612,8 @@ pub async fn execute_puzzle_agent_action( let reference_image_sources = collect_puzzle_reference_image_sources( payload.reference_image_src.as_deref(), payload.reference_image_srcs.as_slice(), + payload.reference_image_asset_object_id.as_deref(), + payload.reference_image_asset_object_ids.as_slice(), ); let primary_reference_image_src = reference_image_sources.first().map(String::as_str); let prompt_text = payload @@ -627,7 +637,7 @@ pub async fn execute_puzzle_agent_action( }; let session = if ai_redraw { execute_billable_asset_operation_with_cost( - &state, + state.root_state(), &owner_user_id, "puzzle_initial_image", &billing_asset_id, @@ -652,7 +662,7 @@ pub async fn execute_puzzle_agent_action( compile_session_id.clone(), owner_user_id.clone(), prompt_text, - payload.reference_image_src.as_deref(), + primary_reference_image_src, now, ) .await @@ -737,7 +747,7 @@ pub async fn execute_puzzle_agent_action( })) }); let session = execute_billable_asset_operation_with_cost( - &state, + state.root_state(), &owner_user_id, "puzzle_generated_image", &billing_asset_id, @@ -768,6 +778,7 @@ pub async fn execute_puzzle_agent_action( let prompt = resolve_puzzle_level_image_prompt( payload.prompt_text.as_deref(), &target_level.picture_description, + &draft.summary, ); let should_auto_name_level = payload .should_auto_name_level @@ -787,26 +798,46 @@ pub async fn execute_puzzle_agent_action( let reference_image_sources = collect_puzzle_reference_image_sources( payload.reference_image_src.as_deref(), payload.reference_image_srcs.as_slice(), + payload.reference_image_asset_object_id.as_deref(), + payload.reference_image_asset_object_ids.as_slice(), ); let primary_reference_image_src = reference_image_sources.first().map(String::as_str); // 拼图结果页从多候选抽卡收口为单图替换,前端传入的旧 candidateCount 只做兼容忽略。 - let candidate_count = 1; let candidate_start_index = target_level.candidates.len(); - let candidates = generate_puzzle_image_candidates( - &state, - owner_user_id.as_str(), - &session.session_id, - &target_level.level_name, - &prompt, + let ai_redraw = payload.ai_redraw.unwrap_or(true); + let mut candidates = if should_use_uploaded_puzzle_image_directly( primary_reference_image_src, - payload.ai_redraw.unwrap_or(true), - payload.image_model.as_deref(), - candidate_count, - candidate_start_index, - ) - .await - .map_err(map_puzzle_generation_endpoint_error)?; + ai_redraw, + ) { + vec![ + create_uploaded_puzzle_image_candidate( + &state, + owner_user_id.as_str(), + &session.session_id, + &target_level.level_name, + &prompt, + primary_reference_image_src.expect("checked reference image"), + candidate_start_index, + ) + .await?, + ] + } else { + generate_puzzle_image_candidates( + &state, + owner_user_id.as_str(), + &session.session_id, + &target_level.level_name, + &prompt, + primary_reference_image_src, + ai_redraw, + payload.image_model.as_deref(), + 1, + candidate_start_index, + ) + .await + .map_err(map_puzzle_generation_endpoint_error)? + }; if candidates.is_empty() { return Err(AppError::from_status(StatusCode::BAD_GATEWAY).with_details( json!({ @@ -831,14 +862,44 @@ pub async fn execute_puzzle_agent_action( generated_naming = Some(refined_naming); } let generated_level_name = target_level.level_name.clone(); + let mut updated_levels = build_puzzle_levels_with_primary_update( + &draft, + &target_level, + primary_reference_image_src, + ); + for candidate in &mut candidates { + candidate.record.prompt = prompt.clone(); + } + let selected_candidate = candidates + .iter() + .find(|candidate| candidate.record.selected) + .or_else(|| candidates.first()) + .ok_or_else(|| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": PUZZLE_AGENT_API_BASE_PROVIDER, + "message": "拼图候选图生成结果为空", + })) + })?; + let asset_bundle = generate_puzzle_level_asset_bundle_required( + &state, + owner_user_id.as_str(), + &session.session_id, + &target_level, + &selected_candidate.downloaded_image, + ) + .await?; + attach_puzzle_level_asset_bundle( + &mut updated_levels, + target_level.level_id.as_str(), + asset_bundle, + ); + attach_selected_puzzle_candidate_to_levels( + &mut updated_levels, + target_level.level_id.as_str(), + &selected_candidate.record, + ); let levels_json_with_generated_name = - Some(serialize_puzzle_level_records_for_module( - &build_puzzle_levels_with_primary_update( - &draft, - &target_level, - primary_reference_image_src, - ), - )?); + Some(serialize_puzzle_level_records_for_module(&updated_levels)?); let candidates_json = serde_json::to_string( &candidates .iter() @@ -890,7 +951,11 @@ pub async fn execute_puzzle_agent_action( }; let mut fallback_session = apply_generated_puzzle_candidates_to_session_snapshot( - fallback_session, + apply_generated_puzzle_levels_to_session_snapshot( + fallback_session, + updated_levels, + now, + ), target_level.level_id.as_str(), candidates.into_records(), primary_reference_image_src, @@ -942,7 +1007,7 @@ pub async fn execute_puzzle_agent_action( })) }); let session = execute_billable_asset_operation_with_cost( - &state, + state.root_state(), &owner_user_id, "puzzle_ui_background_image", &billing_asset_id, @@ -1147,7 +1212,7 @@ pub async fn execute_puzzle_agent_action( let (work_id, profile_id) = build_stable_puzzle_work_ids(&session_id); let author_display_name = resolve_author_display_name(&state, &authenticated); let profile = execute_billable_asset_operation( - &state, + state.root_state(), &owner_user_id, "puzzle_publish_work", &work_id, @@ -1235,7 +1300,7 @@ pub async fn execute_puzzle_agent_action( } pub async fn get_puzzle_works( - State(state): State, + State(state): State, Extension(request_context): Extension, Extension(authenticated): Extension, ) -> Result, Response> { @@ -1263,7 +1328,7 @@ pub async fn get_puzzle_works( } pub async fn get_puzzle_work_detail( - State(state): State, + State(state): State, AxumPath(profile_id): AxumPath, Extension(request_context): Extension, Extension(_authenticated): Extension, @@ -1296,7 +1361,7 @@ pub async fn get_puzzle_work_detail( } pub async fn put_puzzle_work( - State(state): State, + State(state): State, AxumPath(profile_id): AxumPath, Extension(request_context): Extension, Extension(authenticated): Extension, @@ -1355,7 +1420,7 @@ pub async fn put_puzzle_work( } pub async fn delete_puzzle_work( - State(state): State, + State(state): State, AxumPath(profile_id): AxumPath, Extension(request_context): Extension, Extension(authenticated): Extension, @@ -1391,7 +1456,7 @@ pub async fn delete_puzzle_work( } pub async fn claim_puzzle_work_point_incentive( - State(state): State, + State(state): State, AxumPath(profile_id): AxumPath, Extension(request_context): Extension, Extension(authenticated): Extension, @@ -1428,7 +1493,7 @@ pub async fn claim_puzzle_work_point_incentive( } pub async fn list_puzzle_gallery( - State(state): State, + State(state): State, Extension(request_context): Extension, ) -> Result { if let Some(response) = state.puzzle_gallery_cache().read_fresh_response().await { @@ -1487,7 +1552,7 @@ pub async fn list_puzzle_gallery( } pub async fn get_puzzle_gallery_detail( - State(state): State, + State(state): State, AxumPath(profile_id): AxumPath, Extension(request_context): Extension, ) -> Result, Response> { @@ -1519,7 +1584,7 @@ pub async fn get_puzzle_gallery_detail( } pub async fn record_puzzle_gallery_like( - State(state): State, + State(state): State, AxumPath(profile_id): AxumPath, Extension(request_context): Extension, Extension(authenticated): Extension, @@ -1556,7 +1621,7 @@ pub async fn record_puzzle_gallery_like( } pub async fn remix_puzzle_gallery_work( - State(state): State, + State(state): State, AxumPath(profile_id): AxumPath, Extension(request_context): Extension, Extension(authenticated): Extension, @@ -1599,7 +1664,7 @@ pub async fn remix_puzzle_gallery_work( } pub async fn start_puzzle_run( - State(state): State, + State(state): State, Extension(request_context): Extension, Extension(authenticated): Extension, payload: Result, JsonRejection>, @@ -1639,7 +1704,7 @@ pub async fn start_puzzle_run( ) })?; - record_work_play_start_after_success( + record_puzzle_work_play_start_after_success( &state, &request_context, WorkPlayTrackingDraft::new( @@ -1665,7 +1730,7 @@ pub async fn start_puzzle_run( } pub async fn get_puzzle_run( - State(state): State, + State(state): State, AxumPath(run_id): AxumPath, Extension(request_context): Extension, Extension(authenticated): Extension, @@ -1693,7 +1758,7 @@ pub async fn get_puzzle_run( } pub async fn swap_puzzle_pieces( - State(state): State, + State(state): State, AxumPath(run_id): AxumPath, Extension(request_context): Extension, Extension(authenticated): Extension, @@ -1750,7 +1815,7 @@ pub async fn swap_puzzle_pieces( } pub async fn drag_puzzle_piece_or_group( - State(state): State, + State(state): State, AxumPath(run_id): AxumPath, Extension(request_context): Extension, Extension(authenticated): Extension, @@ -1802,7 +1867,7 @@ pub async fn drag_puzzle_piece_or_group( } pub async fn advance_puzzle_next_level( - State(state): State, + State(state): State, AxumPath(run_id): AxumPath, Extension(request_context): Extension, Extension(authenticated): Extension, @@ -1854,7 +1919,7 @@ pub async fn advance_puzzle_next_level( } pub async fn update_puzzle_run_pause( - State(state): State, + State(state): State, AxumPath(run_id): AxumPath, Extension(request_context): Extension, Extension(authenticated): Extension, @@ -1898,7 +1963,7 @@ pub async fn update_puzzle_run_pause( } pub async fn use_puzzle_runtime_prop( - State(state): State, + State(state): State, AxumPath(run_id): AxumPath, Extension(request_context): Extension, Extension(authenticated): Extension, @@ -1944,7 +2009,7 @@ pub async fn use_puzzle_runtime_prop( let fallback_run_id = run_id.clone(); let fallback_owner_user_id = owner_user_id.clone(); let run_result = execute_billable_asset_operation( - &state, + state.root_state(), &owner_user_id, billing_asset_kind, billing_asset_id.as_str(), @@ -1996,7 +2061,7 @@ pub async fn use_puzzle_runtime_prop( } pub async fn submit_puzzle_leaderboard( - State(state): State, + State(state): State, AxumPath(run_id): AxumPath, Extension(request_context): Extension, Extension(authenticated): Extension, diff --git a/server-rs/crates/api-server/src/puzzle/mappers.rs b/server-rs/crates/api-server/src/puzzle/mappers.rs index 6e6c91fd..de8d2994 100644 --- a/server-rs/crates/api-server/src/puzzle/mappers.rs +++ b/server-rs/crates/api-server/src/puzzle/mappers.rs @@ -105,6 +105,12 @@ pub(super) fn map_puzzle_draft_level_response( ui_background_prompt: level.ui_background_prompt, ui_background_image_src: level.ui_background_image_src, ui_background_image_object_key: level.ui_background_image_object_key, + level_scene_image_src: level.level_scene_image_src, + level_scene_image_object_key: level.level_scene_image_object_key, + ui_spritesheet_image_src: level.ui_spritesheet_image_src, + ui_spritesheet_image_object_key: level.ui_spritesheet_image_object_key, + level_background_image_src: level.level_background_image_src, + level_background_image_object_key: level.level_background_image_object_key, background_music: level .background_music .map(map_puzzle_audio_asset_record_response), @@ -343,11 +349,11 @@ fn has_puzzle_level_image(level: &PuzzleDraftLevelRecord) -> bool { } pub(super) fn map_puzzle_work_summary_response( - state: &AppState, + state: &PuzzleApiState, item: PuzzleWorkProfileRecord, ) -> PuzzleWorkSummaryResponse { let generation_status = resolve_puzzle_work_generation_status(&item); - let author = resolve_work_author_by_user_id( + let author = resolve_puzzle_work_author_by_user_id( state, &item.owner_user_id, Some(&item.author_display_name), @@ -391,10 +397,10 @@ pub(super) fn map_puzzle_work_summary_response( } pub(super) fn map_puzzle_gallery_card_response( - state: &AppState, + state: &PuzzleApiState, item: PuzzleGalleryCardRecord, ) -> PuzzleWorkSummaryResponse { - let author = resolve_work_author_by_user_id( + let author = resolve_puzzle_work_author_by_user_id( state, &item.owner_user_id, Some(&item.author_display_name), @@ -434,7 +440,7 @@ pub(super) fn map_puzzle_gallery_card_response( } pub(super) fn map_puzzle_work_profile_response( - state: &AppState, + state: &PuzzleApiState, item: PuzzleWorkProfileRecord, ) -> PuzzleWorkProfileResponse { let mut summary = map_puzzle_work_summary_response(state, item.clone()); @@ -491,7 +497,7 @@ pub(super) fn map_puzzle_recommended_next_work_response( } pub(super) async fn enrich_puzzle_run_author_name( - state: &AppState, + state: &PuzzleApiState, mut run: PuzzleRunRecord, ) -> PuzzleRunRecord { if let Some(level) = run.current_level.as_mut() { @@ -500,7 +506,7 @@ pub(super) async fn enrich_puzzle_run_author_name( .get_puzzle_gallery_detail(level.profile_id.clone()) .await { - level.author_display_name = resolve_work_author_by_user_id( + level.author_display_name = resolve_puzzle_work_author_by_user_id( state, &profile.owner_user_id, Some(&profile.author_display_name), @@ -541,6 +547,10 @@ pub(super) fn map_puzzle_runtime_level_response( cover_image_src: level.cover_image_src, ui_background_image_src: level.ui_background_image_src, ui_background_image_object_key: level.ui_background_image_object_key, + level_background_image_src: level.level_background_image_src, + level_background_image_object_key: level.level_background_image_object_key, + ui_spritesheet_image_src: level.ui_spritesheet_image_src, + ui_spritesheet_image_object_key: level.ui_spritesheet_image_object_key, background_music: level .background_music .map(map_puzzle_audio_asset_record_response), @@ -632,7 +642,7 @@ pub(super) fn map_puzzle_board_response( } pub(super) fn resolve_author_display_name( - state: &AppState, + state: &PuzzleApiState, authenticated: &AuthenticatedAccessToken, ) -> String { state diff --git a/server-rs/crates/api-server/src/puzzle/tags.rs b/server-rs/crates/api-server/src/puzzle/tags.rs index e97f0f44..f49cc84e 100644 --- a/server-rs/crates/api-server/src/puzzle/tags.rs +++ b/server-rs/crates/api-server/src/puzzle/tags.rs @@ -1,7 +1,7 @@ use super::*; pub(super) async fn generate_puzzle_work_tags( - state: &AppState, + state: &PuzzleApiState, work_title: &str, work_description: &str, ) -> Vec { @@ -143,7 +143,7 @@ pub(super) fn build_fallback_puzzle_tags( } pub(super) async fn save_generated_puzzle_tags_to_session( - state: &AppState, + state: &PuzzleApiState, session_id: &str, owner_user_id: &str, payload: &ExecutePuzzleAgentActionRequest, @@ -278,6 +278,12 @@ pub(super) fn serialize_puzzle_level_records_for_module( "ui_background_prompt": level.ui_background_prompt, "ui_background_image_src": level.ui_background_image_src, "ui_background_image_object_key": level.ui_background_image_object_key, + "level_scene_image_src": level.level_scene_image_src, + "level_scene_image_object_key": level.level_scene_image_object_key, + "ui_spritesheet_image_src": level.ui_spritesheet_image_src, + "ui_spritesheet_image_object_key": level.ui_spritesheet_image_object_key, + "level_background_image_src": level.level_background_image_src, + "level_background_image_object_key": level.level_background_image_object_key, "background_music": puzzle_audio_asset_record_module_json(&level.background_music), "candidates": level .candidates diff --git a/server-rs/crates/api-server/src/puzzle/tests.rs b/server-rs/crates/api-server/src/puzzle/tests.rs index 69425e82..e0a780da 100644 --- a/server-rs/crates/api-server/src/puzzle/tests.rs +++ b/server-rs/crates/api-server/src/puzzle/tests.rs @@ -1,4 +1,5 @@ use super::*; +use crate::openai_image_generation::GPT_IMAGE_2_MODEL; #[test] fn puzzle_generated_image_size_is_square_1_1() { @@ -7,7 +8,7 @@ fn puzzle_generated_image_size_is_square_1_1() { } #[test] -fn puzzle_vector_engine_request_uses_gpt_image_2_all_and_reference_images() { +fn puzzle_vector_engine_create_request_uses_gpt_image_2_without_reference_images() { let body = build_puzzle_vector_engine_image_request_body( PuzzleImageModel::Gemini31FlashPreview, "一只猫在雨夜灯牌下回头。", @@ -17,7 +18,7 @@ fn puzzle_vector_engine_request_uses_gpt_image_2_all_and_reference_images() { None, ); - assert_eq!(body["model"], VECTOR_ENGINE_GPT_IMAGE_2_MODEL); + assert_eq!(body["model"], GPT_IMAGE_2_MODEL); assert_eq!(body["size"], PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE); assert_eq!(body["n"], 1); assert!(body.get("official_fallback").is_none()); @@ -31,7 +32,7 @@ fn puzzle_vector_engine_request_uses_gpt_image_2_all_and_reference_images() { } #[test] -fn puzzle_vector_engine_generation_fallback_includes_reference_image() { +fn puzzle_vector_engine_create_request_never_embeds_reference_image() { let image = image::DynamicImage::ImageRgb8(image::RgbImage::new(4, 4)); let mut cursor = std::io::Cursor::new(Vec::new()); image @@ -41,6 +42,7 @@ fn puzzle_vector_engine_generation_fallback_includes_reference_image() { mime_type: "image/png".to_string(), bytes_len: cursor.get_ref().len(), bytes: cursor.into_inner(), + signed_read_url: None, }; let body = build_puzzle_vector_engine_image_request_body( @@ -52,20 +54,185 @@ fn puzzle_vector_engine_generation_fallback_includes_reference_image() { Some(&reference_image), ); - let images = body["image"] - .as_array() - .expect("fallback generation should include reference image array"); - assert_eq!(images.len(), 1); + assert!(body.get("image").is_none()); +} + +#[test] +fn puzzle_level_scene_spritesheet_and_background_requests_use_references() { + let image = image::DynamicImage::ImageRgb8(image::RgbImage::new(4, 4)); + let mut cursor = std::io::Cursor::new(Vec::new()); + image + .write_to(&mut cursor, ImageFormat::Png) + .expect("test image should encode"); + let reference_image = PuzzleDownloadedImage { + extension: "png".to_string(), + mime_type: "image/png".to_string(), + bytes: cursor.into_inner(), + }; + + let scene_body = build_puzzle_level_scene_image_request_body_for_test(&reference_image) + .expect("scene request should build"); + assert_eq!(scene_body["model"], GPT_IMAGE_2_MODEL); + assert_eq!(scene_body["size"], "1024x1536"); + assert!(scene_body.get("image").is_none()); assert!( - images[0] + scene_body["prompt"] .as_str() .unwrap_or_default() - .starts_with("data:image/png;base64,") + .contains("参考图作为拼图画面") + ); + assert!( + scene_body["prompt"] + .as_str() + .unwrap_or_default() + .contains("道具按钮上不要显示次数标注") + ); + assert!( + scene_body["prompt"] + .as_str() + .unwrap_or_default() + .contains("返回按钮和设置按钮旁禁止标注文字") + ); + + let spritesheet_body = build_puzzle_ui_spritesheet_request_body_for_test(&reference_image) + .expect("spritesheet request should build"); + assert_eq!(spritesheet_body["model"], GPT_IMAGE_2_MODEL); + assert_eq!(spritesheet_body["size"], "1024x1024"); + assert!( + spritesheet_body["prompt"] + .as_str() + .unwrap_or_default() + .contains("返回按钮、设置按钮、下一关按钮、提示按钮、原图按钮、冻结按钮") + ); + assert!( + spritesheet_body["prompt"] + .as_str() + .unwrap_or_default() + .contains("纯绿色绿幕背景") + ); + assert!( + spritesheet_body["prompt"] + .as_str() + .unwrap_or_default() + .contains("绿幕扣成透明") + ); + assert!( + spritesheet_body["prompt"] + .as_str() + .unwrap_or_default() + .contains("自动边界检测") + ); + assert!( + spritesheet_body["prompt"] + .as_str() + .unwrap_or_default() + .contains("按钮素材内必须保留对应中文文字") + ); + assert!( + spritesheet_body["prompt"] + .as_str() + .unwrap_or_default() + .contains("不要额外画白色外圈") + ); + assert!( + spritesheet_body["prompt"] + .as_str() + .unwrap_or_default() + .contains("白底圆环") + ); + assert!( + !spritesheet_body["prompt"] + .as_str() + .unwrap_or_default() + .contains("禁止文字") + ); + + let background_body = build_puzzle_level_background_request_body_for_test(&reference_image) + .expect("background request should build"); + assert_eq!(background_body["model"], GPT_IMAGE_2_MODEL); + assert_eq!(background_body["size"], "1024x1536"); + assert!( + background_body["prompt"] + .as_str() + .unwrap_or_default() + .contains("移除参考图中所有UI元素") + ); + assert!( + background_body["prompt"] + .as_str() + .unwrap_or_default() + .contains("禁止在背景中出现人像或和拼图画面中主体一致的内容") ); } #[test] -fn puzzle_vector_engine_edit_url_uses_images_edits_endpoint() { +fn puzzle_ui_spritesheet_postprocess_turns_green_screen_transparent() { + let mut source = image::RgbaImage::from_pixel(8, 8, image::Rgba([0, 255, 0, 255])); + for y in 2..6 { + for x in 2..6 { + source.put_pixel(x, y, image::Rgba([190, 78, 42, 255])); + } + } + let mut cursor = std::io::Cursor::new(Vec::new()); + image::DynamicImage::ImageRgba8(source) + .write_to(&mut cursor, ImageFormat::Png) + .expect("test image should encode"); + + let processed = make_puzzle_ui_spritesheet_image_transparent_for_test(PuzzleDownloadedImage { + extension: "png".to_string(), + mime_type: "image/png".to_string(), + bytes: cursor.into_inner(), + }) + .expect("green screen postprocess should succeed"); + + assert_eq!(processed.extension, "png"); + assert_eq!(processed.mime_type, "image/png"); + let decoded = image::load_from_memory(processed.bytes.as_slice()) + .expect("processed image should decode") + .to_rgba8(); + assert_eq!(decoded.get_pixel(0, 0).0[3], 0); + assert_eq!(decoded.get_pixel(3, 3).0[3], 255); +} + +#[test] +fn puzzle_vector_engine_create_request_never_embeds_signed_reference_url() { + let reference_image = PuzzleResolvedReferenceImage { + mime_type: "image/png".to_string(), + bytes_len: 4, + bytes: b"test".to_vec(), + signed_read_url: Some( + "https://oss.example/generated-puzzle-assets/reference.png?x-oss-signature=abc" + .to_string(), + ), + }; + + let body = build_puzzle_vector_engine_image_request_body( + PuzzleImageModel::GptImage2, + "参考图里的小猫做成拼图主图。", + PUZZLE_DEFAULT_NEGATIVE_PROMPT, + PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE, + 1, + Some(&reference_image), + ); + + assert!(body.get("image").is_none()); +} + +#[test] +fn puzzle_vector_engine_generation_url_normalizes_base_url() { + let settings = PuzzleVectorEngineSettings { + base_url: "https://vector.example/v1".to_string(), + api_key: "test-key".to_string(), + }; + + assert_eq!( + puzzle_vector_engine_images_generation_url(&settings), + "https://vector.example/v1/images/generations" + ); +} + +#[test] +fn puzzle_vector_engine_edit_url_normalizes_base_url() { let settings = PuzzleVectorEngineSettings { base_url: "https://vector.example/v1".to_string(), api_key: "test-key".to_string(), @@ -107,18 +274,31 @@ fn puzzle_vector_engine_prompt_keeps_text_only_prompt_unchanged() { } #[test] -fn puzzle_reference_image_edit_requires_ai_redraw() { - assert!(!should_use_puzzle_reference_image_edit(None, true)); - assert!(!should_use_puzzle_reference_image_edit( +fn puzzle_reference_image_generation_requires_ai_redraw() { + assert!(!should_use_puzzle_reference_image_generation(None, true)); + assert!(!should_use_puzzle_reference_image_generation( Some("data:image/png;base64,abcd"), false )); - assert!(should_use_puzzle_reference_image_edit( + assert!(should_use_puzzle_reference_image_generation( Some("data:image/png;base64,abcd"), true )); } +#[test] +fn puzzle_result_level_direct_upload_skips_cover_image_generation() { + assert!(should_use_uploaded_puzzle_image_directly( + Some("data:image/png;base64,abcd"), + false + )); + assert!(!should_use_uploaded_puzzle_image_directly( + Some("data:image/png;base64,abcd"), + true + )); + assert!(!should_use_uploaded_puzzle_image_directly(None, false)); +} + #[test] fn puzzle_reference_image_sources_are_deduped_and_limited() { let sources = collect_puzzle_reference_image_sources( @@ -131,6 +311,8 @@ fn puzzle_reference_image_sources_are_deduped_and_limited() { "data:image/png;base64,e".to_string(), "data:image/png;base64,f".to_string(), ], + None, + &[], ); assert_eq!(sources.len(), 5); @@ -139,6 +321,62 @@ fn puzzle_reference_image_sources_are_deduped_and_limited() { assert!(!sources.contains(&"data:image/png;base64,f".to_string())); } +#[test] +fn puzzle_reference_image_sources_prefer_asset_object_ids() { + let sources = collect_puzzle_reference_image_sources( + Some("data:image/png;base64,legacy"), + &["/generated-puzzle-assets/legacy.png".to_string()], + Some("asset-main-1"), + &[ + "asset-main-1".to_string(), + "asset-prompt-1".to_string(), + "asset-prompt-2".to_string(), + ], + ); + + assert_eq!( + sources, + vec![ + "asset-object:asset-main-1".to_string(), + "asset-object:asset-prompt-1".to_string(), + "asset-object:asset-prompt-2".to_string(), + "data:image/png;base64,legacy".to_string(), + "/generated-puzzle-assets/legacy.png".to_string(), + ] + ); +} + +#[test] +fn puzzle_asset_object_reference_requires_matching_owner() { + let asset_object = module_assets::AssetObjectRecord { + asset_object_id: "assetobj_reference_1".to_string(), + bucket: "genarrative-assets".to_string(), + object_key: "generated-puzzle-assets/reference/image.png".to_string(), + access_policy: module_assets::AssetObjectAccessPolicy::Private, + content_type: Some("image/png".to_string()), + content_length: 1024, + content_hash: None, + version: 1, + source_job_id: None, + owner_user_id: Some("user-other".to_string()), + profile_id: None, + entity_id: None, + asset_kind: "puzzle_cover_image".to_string(), + created_at: "2026-05-21T00:00:00Z".to_string(), + updated_at: "2026-05-21T00:00:00Z".to_string(), + }; + + let error = validate_puzzle_reference_asset_object( + &asset_object, + Some("user-current"), + "genarrative-assets", + ) + .expect_err("其他账号的参考图资产应被拒绝"); + + assert_eq!(error.status_code(), StatusCode::FORBIDDEN); + assert!(error.body_text().contains("不属于当前账号")); +} + #[test] fn puzzle_vector_engine_timeout_maps_to_gateway_timeout() { let error = map_puzzle_vector_engine_request_error( @@ -153,51 +391,14 @@ fn puzzle_vector_engine_timeout_maps_to_gateway_timeout() { fn puzzle_vector_engine_upstream_timeout_maps_to_gateway_timeout() { let error = map_puzzle_vector_engine_upstream_error( reqwest::StatusCode::GATEWAY_TIMEOUT, - r#"{"error":{"message":"VectorEngine edit endpoint timeout"}}"#, - "创建拼图 VectorEngine 图片编辑任务失败", + r#"{"error":{"message":"VectorEngine generation endpoint timeout"}}"#, + "创建拼图 VectorEngine 图片生成任务失败", ); let response = error.into_response(); assert_eq!(response.status(), StatusCode::GATEWAY_TIMEOUT); } -#[test] -fn puzzle_reference_edit_fallback_only_accepts_timeout_errors() { - let timeout_error = map_puzzle_vector_engine_upstream_error( - reqwest::StatusCode::GATEWAY_TIMEOUT, - r#"{"error":{"message":"VectorEngine edit endpoint timeout"}}"#, - "创建拼图 VectorEngine 图片编辑任务失败", - ); - assert!(should_fallback_puzzle_reference_edit_to_generation( - &timeout_error - )); - - let auth_error = map_puzzle_vector_engine_upstream_error( - reqwest::StatusCode::UNAUTHORIZED, - r#"{"error":{"message":"invalid api key"}}"#, - "创建拼图 VectorEngine 图片编辑任务失败", - ); - assert!(!should_fallback_puzzle_reference_edit_to_generation( - &auth_error - )); -} - -#[test] -fn puzzle_vector_engine_reqwest_error_maps_to_bad_gateway() { - let error = match reqwest::Client::new().get("http://[::1").build() { - Ok(_) => panic!("invalid url should fail request build"), - Err(error) => error, - }; - let app_error = map_puzzle_vector_engine_reqwest_error( - "创建拼图 VectorEngine 图片编辑任务失败", - "https://api.vectorengine.ai/v1/images/edits", - error, - ); - - let response = app_error.into_response(); - assert_eq!(response.status(), StatusCode::BAD_GATEWAY); -} - #[test] fn puzzle_compile_error_preserves_vector_engine_unavailable_status() { let error = map_puzzle_compile_error(SpacetimeClientError::Runtime( @@ -250,6 +451,8 @@ fn puzzle_image_generation_builds_fallback_session_from_levels_snapshot() { prompt_text: None, reference_image_src: None, reference_image_srcs: Vec::new(), + reference_image_asset_object_id: None, + reference_image_asset_object_ids: Vec::new(), image_model: Some(PUZZLE_IMAGE_MODEL_GPT_IMAGE_2.to_string()), ai_redraw: None, candidate_count: Some(1), @@ -383,6 +586,7 @@ fn puzzle_uploaded_cover_can_reuse_resolved_history_image() { mime_type: "image/png".to_string(), bytes_len: 8, bytes: b"pngbytes".to_vec(), + signed_read_url: None, }; let downloaded = PuzzleDownloadedImage::from_resolved_reference_image(resolved); @@ -410,6 +614,8 @@ fn puzzle_first_level_name_snapshot_defaults_work_title() { prompt_text: None, reference_image_src: None, reference_image_srcs: Vec::new(), + reference_image_asset_object_id: None, + reference_image_asset_object_ids: Vec::new(), image_model: Some(PUZZLE_IMAGE_MODEL_GPT_IMAGE_2.to_string()), ai_redraw: None, candidate_count: Some(1), @@ -510,6 +716,12 @@ fn puzzle_level_audio_asset_roundtrips_between_response_and_module_json() { ui_background_prompt: None, ui_background_image_src: None, ui_background_image_object_key: None, + level_scene_image_src: None, + level_scene_image_object_key: None, + ui_spritesheet_image_src: None, + ui_spritesheet_image_object_key: None, + level_background_image_src: None, + level_background_image_object_key: None, background_music: Some(CreationAudioAsset { task_id: "suno-task-1".to_string(), provider: "vector-engine-suno".to_string(), @@ -575,6 +787,12 @@ fn puzzle_ui_background_fields_roundtrip_between_response_and_module_json() { ui_background_image_object_key: Some( "generated-puzzle-assets/session/ui/background.png".to_string(), ), + level_scene_image_src: None, + level_scene_image_object_key: None, + ui_spritesheet_image_src: None, + ui_spritesheet_image_object_key: None, + level_background_image_src: None, + level_background_image_object_key: None, background_music: None, candidates: vec![], selected_candidate_id: None, @@ -612,9 +830,86 @@ fn puzzle_ui_background_fields_roundtrip_between_response_and_module_json() { ); } +#[test] +fn puzzle_level_asset_bundle_fields_roundtrip_between_response_and_module_json() { + let level = PuzzleDraftLevelResponse { + level_id: "puzzle-level-1".to_string(), + level_name: "雨夜猫街".to_string(), + picture_description: "一只猫在雨夜灯牌下回头。".to_string(), + picture_reference: None, + ui_background_prompt: Some("雨夜猫街竖屏拼图UI背景".to_string()), + ui_background_image_src: Some( + "/generated-puzzle-assets/session/legacy-ui/background.png".to_string(), + ), + ui_background_image_object_key: Some( + "generated-puzzle-assets/session/legacy-ui/background.png".to_string(), + ), + level_scene_image_src: Some( + "/generated-puzzle-assets/session/level-scene/scene.png".to_string(), + ), + level_scene_image_object_key: Some( + "generated-puzzle-assets/session/level-scene/scene.png".to_string(), + ), + ui_spritesheet_image_src: Some( + "/generated-puzzle-assets/session/ui-spritesheet/sheet.png".to_string(), + ), + ui_spritesheet_image_object_key: Some( + "generated-puzzle-assets/session/ui-spritesheet/sheet.png".to_string(), + ), + level_background_image_src: Some( + "/generated-puzzle-assets/session/level-background/background.png".to_string(), + ), + level_background_image_object_key: Some( + "generated-puzzle-assets/session/level-background/background.png".to_string(), + ), + background_music: None, + candidates: vec![], + selected_candidate_id: None, + cover_image_src: Some("/generated-puzzle-assets/session/cover.png".to_string()), + cover_asset_id: Some("asset-1".to_string()), + generation_status: "ready".to_string(), + }; + let request_context = RequestContext::new( + "test-request".to_string(), + "PUT /api/runtime/puzzle/works/test".to_string(), + Duration::ZERO, + false, + ); + + 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"); + assert_eq!( + payload[0]["level_background_image_object_key"], + Value::String( + "generated-puzzle-assets/session/level-background/background.png".to_string() + ) + ); + assert!(payload[0].get("levelBackgroundImageObjectKey").is_none()); + + let records = parse_puzzle_level_records_from_module_json(&levels_json) + .expect("levels should map back into records"); + assert_eq!( + records[0].level_scene_image_src.as_deref(), + Some("/generated-puzzle-assets/session/level-scene/scene.png") + ); + assert_eq!( + records[0].ui_spritesheet_image_object_key.as_deref(), + Some("generated-puzzle-assets/session/ui-spritesheet/sheet.png") + ); + + let response = map_puzzle_draft_level_response(records[0].clone()); + assert_eq!( + response.level_background_image_src.as_deref(), + Some("/generated-puzzle-assets/session/level-background/background.png") + ); +} + #[test] fn puzzle_work_summary_response_keeps_levels_for_shelf_cover() { - let state = AppState::new(crate::config::AppConfig::default()).expect("state should build"); + let app_state = crate::state::AppState::new(crate::config::AppConfig::default()) + .expect("state should build"); + let state: PuzzleApiState = axum::extract::FromRef::from_ref(&app_state); let level = PuzzleDraftLevelRecord { level_id: "puzzle-level-1".to_string(), level_name: "雨夜猫街".to_string(), @@ -623,6 +918,12 @@ fn puzzle_work_summary_response_keeps_levels_for_shelf_cover() { ui_background_prompt: None, ui_background_image_src: None, ui_background_image_object_key: None, + level_scene_image_src: None, + level_scene_image_object_key: None, + ui_spritesheet_image_src: None, + ui_spritesheet_image_object_key: None, + level_background_image_src: None, + level_background_image_object_key: None, background_music: None, candidates: vec![PuzzleGeneratedImageCandidateRecord { candidate_id: "candidate-1".to_string(), @@ -756,12 +1057,15 @@ fn puzzle_initial_draft_assets_must_include_ui_background() { let missing_all = ensure_puzzle_initial_level_assets_ready(&draft.levels[0]) .expect_err("缺少自动生成资产时不能把草稿标记为完成"); assert_eq!(missing_all.status_code(), StatusCode::BAD_GATEWAY); - assert!(missing_all.body_text().contains("UI背景图")); + assert!(missing_all.body_text().contains("关卡背景图")); + assert!(missing_all.body_text().contains("UI spritesheet")); - draft.levels[0].ui_background_image_src = - Some("/generated-puzzle-assets/session/ui/background.png".to_string()); + draft.levels[0].level_background_image_src = + Some("/generated-puzzle-assets/session/background/background.png".to_string()); + draft.levels[0].ui_spritesheet_image_src = + Some("/generated-puzzle-assets/session/spritesheet/sheet.png".to_string()); ensure_puzzle_initial_level_assets_ready(&draft.levels[0]) - .expect("UI 背景存在时即可完成自动草稿资源检查"); + .expect("关卡背景和 UI spritesheet 存在时即可完成自动草稿资源检查"); } fn test_puzzle_anchor_pack_record() -> PuzzleAnchorPackRecord { @@ -805,6 +1109,12 @@ fn test_puzzle_draft_record() -> PuzzleResultDraftRecord { ui_background_prompt: None, ui_background_image_src: None, ui_background_image_object_key: None, + level_scene_image_src: None, + level_scene_image_object_key: None, + ui_spritesheet_image_src: None, + ui_spritesheet_image_object_key: None, + level_background_image_src: None, + level_background_image_object_key: None, background_music: None, candidates: vec![], selected_candidate_id: None, diff --git a/server-rs/crates/api-server/src/puzzle/vector_engine.rs b/server-rs/crates/api-server/src/puzzle/vector_engine.rs index 40383193..85ed78c1 100644 --- a/server-rs/crates/api-server/src/puzzle/vector_engine.rs +++ b/server-rs/crates/api-server/src/puzzle/vector_engine.rs @@ -12,7 +12,7 @@ impl PuzzleImageModel { } pub(crate) fn request_model_name(self) -> &'static str { - VECTOR_ENGINE_GPT_IMAGE_2_MODEL + GPT_IMAGE_2_MODEL } pub(crate) fn candidate_source_type(self) -> &'static str { @@ -37,6 +37,7 @@ pub(crate) struct PuzzleResolvedReferenceImage { pub(crate) mime_type: String, pub(crate) bytes_len: usize, pub(crate) bytes: Vec, + pub(crate) signed_read_url: Option, } pub(crate) struct GeneratedPuzzleImageCandidate { @@ -94,13 +95,26 @@ pub(crate) struct GeneratedPuzzleUiBackgroundResponse { pub(crate) object_key: String, } +#[derive(Clone, Debug)] +pub(crate) struct GeneratedPuzzleLevelAssetResponse { + pub(crate) image_src: String, + pub(crate) object_key: String, +} + +#[derive(Clone, Debug)] +pub(crate) struct GeneratedPuzzleLevelAssetBundle { + pub(crate) level_scene: GeneratedPuzzleLevelAssetResponse, + pub(crate) ui_spritesheet: GeneratedPuzzleLevelAssetResponse, + pub(crate) level_background: GeneratedPuzzleLevelAssetResponse, +} + pub(crate) fn resolve_puzzle_image_model(value: Option<&str>) -> PuzzleImageModel { match value.map(str::trim).filter(|value| !value.is_empty()) { Some(PUZZLE_IMAGE_MODEL_GEMINI_31_FLASH_PREVIEW) => { tracing::warn!( requested_model = PUZZLE_IMAGE_MODEL_GEMINI_31_FLASH_PREVIEW, - effective_model = VECTOR_ENGINE_GPT_IMAGE_2_MODEL, - "拼图 nanobanana2 历史选项已回落到 VectorEngine GPT-image-2-all" + effective_model = GPT_IMAGE_2_MODEL, + "拼图 nanobanana2 历史选项已回落到 VectorEngine GPT-image-2" ); PuzzleImageModel::Gemini31FlashPreview } @@ -109,13 +123,9 @@ pub(crate) fn resolve_puzzle_image_model(value: Option<&str>) -> PuzzleImageMode } pub(crate) fn require_puzzle_vector_engine_settings( - state: &AppState, + state: &PuzzleApiState, ) -> Result { - let base_url = state - .config - .vector_engine_base_url - .trim() - .trim_end_matches('/'); + let base_url = state.vector_engine_base_url().trim().trim_end_matches('/'); if base_url.is_empty() { return Err( AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({ @@ -127,9 +137,7 @@ pub(crate) fn require_puzzle_vector_engine_settings( } let api_key = state - .config - .vector_engine_api_key - .as_deref() + .vector_engine_api_key() .map(str::trim) .filter(|value| !value.is_empty()) .ok_or_else(|| { @@ -147,15 +155,15 @@ pub(crate) fn require_puzzle_vector_engine_settings( } pub(crate) fn build_puzzle_image_http_client( - state: &AppState, + state: &PuzzleApiState, image_model: PuzzleImageModel, ) -> Result { let provider = image_model.provider_name(); - let request_timeout_ms = state.config.vector_engine_image_request_timeout_ms; + let request_timeout_ms = state.vector_engine_image_request_timeout_ms(); reqwest::Client::builder() .timeout(Duration::from_millis(request_timeout_ms.max(1))) - // 中文注释:VectorEngine 的图片编辑接口是 multipart 请求;强制 HTTP/1.1 可避开部分网关对 HTTP/2 multipart 流的中断兼容问题。 + // 中文注释:参考图走 multipart edits;强制 HTTP/1.1 可降低部分网关对长耗时上传流的兼容风险。 .http1_only() .build() .map_err(|error| { @@ -191,6 +199,20 @@ pub(crate) async fn create_puzzle_vector_engine_image_generation( candidate_count: u32, reference_image: Option<&PuzzleResolvedReferenceImage>, ) -> Result { + if let Some(reference_image) = reference_image { + return create_puzzle_vector_engine_image_edit( + http_client, + settings, + image_model, + prompt, + negative_prompt, + size, + candidate_count, + reference_image, + ) + .await; + } + let request_body = build_puzzle_vector_engine_image_request_body( image_model, prompt, @@ -267,6 +289,15 @@ pub(crate) async fn create_puzzle_vector_engine_image_generation( return Ok(images); } + let b64_images = extract_puzzle_b64_images(&payload); + if !b64_images.is_empty() { + return Ok(puzzle_images_from_base64( + format!("vector-engine-{}", current_utc_micros()), + b64_images, + candidate_count, + )); + } + Err( AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ "provider": VECTOR_ENGINE_PROVIDER, @@ -278,6 +309,7 @@ pub(crate) async fn create_puzzle_vector_engine_image_generation( pub(crate) async fn create_puzzle_vector_engine_image_edit( http_client: &reqwest::Client, settings: &PuzzleVectorEngineSettings, + image_model: PuzzleImageModel, prompt: &str, negative_prompt: &str, size: &str, @@ -300,7 +332,7 @@ pub(crate) async fn create_puzzle_vector_engine_image_edit( })?; let form = reqwest::multipart::Form::new() .part("image", image_part) - .text("model", PUZZLE_VECTOR_ENGINE_IMAGE_EDIT_MODEL.to_string()) + .text("model", image_model.request_model_name().to_string()) .text( "prompt", build_puzzle_vector_engine_prompt(prompt, negative_prompt), @@ -319,16 +351,14 @@ pub(crate) async fn create_puzzle_vector_engine_image_edit( .send() .await .map_err(|error| { - map_puzzle_vector_engine_reqwest_error( - "创建拼图 VectorEngine 图片编辑任务失败", - &request_url, - error, - ) + map_puzzle_vector_engine_request_error(format!( + "创建拼图 VectorEngine 图片编辑任务失败:{error}" + )) })?; let status = response.status(); tracing::info!( provider = VECTOR_ENGINE_PROVIDER, - image_model = PUZZLE_VECTOR_ENGINE_IMAGE_EDIT_MODEL, + image_model = image_model.request_model_name(), endpoint = %request_url, status = status.as_u16(), prompt_chars = prompt.chars().count(), @@ -377,6 +407,17 @@ pub(crate) async fn create_puzzle_vector_engine_image_edit( ) } +pub(crate) fn build_puzzle_downloaded_image_reference( + image: &PuzzleDownloadedImage, +) -> PuzzleResolvedReferenceImage { + PuzzleResolvedReferenceImage { + mime_type: image.mime_type.clone(), + bytes_len: image.bytes.len(), + bytes: image.bytes.clone(), + signed_read_url: None, + } +} + pub(crate) fn build_puzzle_vector_engine_image_request_body( image_model: PuzzleImageModel, prompt: &str, @@ -385,7 +426,7 @@ pub(crate) fn build_puzzle_vector_engine_image_request_body( candidate_count: u32, reference_image: Option<&PuzzleResolvedReferenceImage>, ) -> Value { - let mut body = Map::from_iter([ + let body = Map::from_iter([ ( "model".to_string(), Value::String(image_model.request_model_name().to_string()), @@ -397,12 +438,7 @@ pub(crate) fn build_puzzle_vector_engine_image_request_body( ("n".to_string(), json!(candidate_count.clamp(1, 1))), ("size".to_string(), Value::String(size.to_string())), ]); - if let Some(reference_image) = reference_image - && let Some(reference_data_url) = - build_puzzle_generation_reference_image_data_url(reference_image) - { - body.insert("image".to_string(), json!([reference_data_url])); - } + let _ = reference_image; Value::Object(body) } @@ -426,32 +462,6 @@ pub(crate) fn build_puzzle_vector_engine_generation_prompt( ) } -pub(crate) fn build_puzzle_generation_reference_image_data_url( - image: &PuzzleResolvedReferenceImage, -) -> Option { - let bytes = resize_puzzle_generation_reference_image_bytes(image.bytes.as_slice()) - .unwrap_or_else(|| image.bytes.clone()); - let mime_type = if bytes.starts_with(b"\x89PNG\r\n\x1A\n") { - "image/png" - } else { - image.mime_type.as_str() - }; - - Some(format!( - "data:{};base64,{}", - normalize_puzzle_downloaded_image_mime_type(mime_type), - BASE64_STANDARD.encode(bytes) - )) -} - -pub(crate) fn resize_puzzle_generation_reference_image_bytes(bytes: &[u8]) -> Option> { - let image = image::load_from_memory(bytes).ok()?; - let resized = image.resize(1024, 1024, image::imageops::FilterType::Triangle); - let mut cursor = std::io::Cursor::new(Vec::new()); - resized.write_to(&mut cursor, ImageFormat::Png).ok()?; - Some(cursor.into_inner()) -} - pub(crate) fn has_puzzle_reference_image(reference_image_src: Option<&str>) -> bool { reference_image_src .map(str::trim) @@ -462,6 +472,48 @@ pub(crate) fn has_puzzle_reference_image(reference_image_src: Option<&str>) -> b pub(crate) fn collect_puzzle_reference_image_sources( legacy_reference_image_src: Option<&str>, reference_image_srcs: &[String], + reference_image_asset_object_id: Option<&str>, + reference_image_asset_object_ids: &[String], +) -> Vec { + let mut sources = Vec::new(); + for source in reference_image_asset_object_id + .into_iter() + .chain(reference_image_asset_object_ids.iter().map(String::as_str)) + .map(|asset_object_id| { + asset_object_id + .trim() + .strip_prefix("asset-object:") + .unwrap_or_else(|| asset_object_id.trim()) + }) + .filter(|asset_object_id| !asset_object_id.is_empty()) + .map(|asset_object_id| format!("asset-object:{asset_object_id}")) + .chain( + legacy_reference_image_src + .into_iter() + .chain(reference_image_srcs.iter().map(String::as_str)) + .map(str::to_string), + ) + { + let normalized = source.trim(); + if normalized.is_empty() { + continue; + } + if !sources + .iter() + .any(|existing: &String| existing == normalized) + { + sources.push(normalized.to_string()); + } + if sources.len() >= PUZZLE_REFERENCE_IMAGE_SOURCE_LIMIT { + break; + } + } + sources +} + +pub(crate) fn collect_legacy_puzzle_reference_image_sources( + legacy_reference_image_src: Option<&str>, + reference_image_srcs: &[String], ) -> Vec { let mut sources = Vec::new(); for source in legacy_reference_image_src @@ -488,16 +540,23 @@ pub(crate) fn collect_puzzle_reference_image_sources( pub(crate) fn has_puzzle_reference_images( legacy_reference_image_src: Option<&str>, reference_image_srcs: &[String], + reference_image_asset_object_id: Option<&str>, + reference_image_asset_object_ids: &[String], ) -> bool { - !collect_puzzle_reference_image_sources(legacy_reference_image_src, reference_image_srcs) - .is_empty() + !collect_puzzle_reference_image_sources( + legacy_reference_image_src, + reference_image_srcs, + reference_image_asset_object_id, + reference_image_asset_object_ids, + ) + .is_empty() } -pub(crate) fn should_use_puzzle_reference_image_edit( +pub(crate) fn should_use_puzzle_reference_image_generation( reference_image_src: Option<&str>, - use_reference_image_edit: bool, + use_reference_image_generation: bool, ) -> bool { - use_reference_image_edit && has_puzzle_reference_image(reference_image_src) + use_reference_image_generation && has_puzzle_reference_image(reference_image_src) } pub(crate) fn build_puzzle_vector_engine_prompt(prompt: &str, negative_prompt: &str) -> String { @@ -546,10 +605,19 @@ pub(crate) async fn download_puzzle_images_from_urls( Ok(PuzzleGeneratedImages { task_id, images }) } -pub(crate) async fn resolve_puzzle_reference_image_as_data_url( - state: &AppState, +pub(crate) fn parse_puzzle_asset_object_reference(source: &str) -> Option<&str> { + source + .trim() + .strip_prefix("asset-object:") + .map(str::trim) + .filter(|value| !value.is_empty()) +} + +pub(crate) async fn resolve_puzzle_reference_image( + state: &PuzzleApiState, http_client: &reqwest::Client, source: &str, + owner_user_id: Option<&str>, ) -> Result { let trimmed = source.trim(); if trimmed.is_empty() { @@ -562,6 +630,16 @@ pub(crate) async fn resolve_puzzle_reference_image_as_data_url( ); } + if let Some(asset_object_id) = parse_puzzle_asset_object_reference(trimmed) { + return resolve_puzzle_reference_asset_object( + state, + http_client, + asset_object_id, + owner_user_id, + ) + .await; + } + if let Some(parsed) = parse_puzzle_image_data_url(trimmed) { let bytes_len = parsed.bytes.len(); if bytes_len > PUZZLE_REFERENCE_IMAGE_MAX_BYTES { @@ -579,6 +657,7 @@ pub(crate) async fn resolve_puzzle_reference_image_as_data_url( mime_type: parsed.mime_type, bytes_len, bytes: parsed.bytes, + signed_read_url: None, }); } @@ -587,7 +666,7 @@ pub(crate) async fn resolve_puzzle_reference_image_as_data_url( AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "puzzle", "field": "referenceImageSrc", - "message": "参考图必须是 Data URL 或 /generated-* 旧路径。", + "message": "参考图必须是 assetObjectId、Data URL 或 /generated-* 旧路径。", })), ); } @@ -598,7 +677,7 @@ pub(crate) async fn resolve_puzzle_reference_image_as_data_url( AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ "provider": "puzzle", "field": "referenceImageSrc", - "message": "参考图当前只支持 /generated-* 旧路径。", + "message": "参考图当前只支持 assetObjectId 或 /generated-* 旧路径。", })), ); } @@ -615,8 +694,159 @@ pub(crate) async fn resolve_puzzle_reference_image_as_data_url( expire_seconds: Some(60), }) .map_err(map_puzzle_asset_oss_error)?; + let signed_read_url = signed.signed_url; + download_signed_puzzle_reference_image( + http_client, + signed_read_url, + object_key, + None, + "referenceImageSrc", + ) + .await +} + +pub(crate) async fn resolve_puzzle_reference_image_as_data_url( + state: &PuzzleApiState, + http_client: &reqwest::Client, + source: &str, +) -> Result { + resolve_puzzle_reference_image(state, http_client, source, None).await +} + +async fn resolve_puzzle_reference_asset_object( + state: &PuzzleApiState, + http_client: &reqwest::Client, + asset_object_id: &str, + owner_user_id: Option<&str>, +) -> Result { + let asset_object = state + .spacetime_client() + .get_asset_object(asset_object_id.to_string()) + .await + .map_err(map_puzzle_client_error)? + .ok_or_else(|| { + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "asset-object", + "field": "referenceImageAssetObjectId", + "assetObjectId": asset_object_id, + "message": "参考图资产不存在或当前账号不可见。", + })) + })?; + let oss_client = state.oss_client().ok_or_else(|| { + AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({ + "provider": "aliyun-oss", + "reason": "OSS 未完成环境变量配置", + })) + })?; + validate_puzzle_reference_asset_object( + &asset_object, + owner_user_id, + oss_client.config_bucket(), + )?; + let signed = oss_client + .sign_get_object_url(OssSignedGetObjectUrlRequest { + object_key: asset_object.object_key.clone(), + expire_seconds: Some(60), + }) + .map_err(map_puzzle_asset_oss_error)?; + let content_type = asset_object.content_type.clone(); + download_signed_puzzle_reference_image( + http_client, + signed.signed_url, + asset_object.object_key.as_str(), + content_type.as_deref(), + "referenceImageAssetObjectId", + ) + .await +} + +pub(crate) fn validate_puzzle_reference_asset_object( + asset_object: &module_assets::AssetObjectRecord, + owner_user_id: Option<&str>, + oss_bucket: &str, +) -> Result<(), AppError> { + if asset_object.bucket.trim() != oss_bucket.trim() { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "asset-object", + "field": "referenceImageAssetObjectId", + "assetObjectId": asset_object.asset_object_id, + "message": "参考图资产 bucket 与当前服务 OSS 配置不一致。", + })), + ); + } + if asset_object.asset_kind.trim() != "puzzle_cover_image" { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "asset-object", + "field": "referenceImageAssetObjectId", + "assetObjectId": asset_object.asset_object_id, + "message": "参考图资产类型不属于拼图图片。", + })), + ); + } + let content_type = asset_object + .content_type + .as_deref() + .map(str::trim) + .unwrap_or_default(); + if !content_type.starts_with("image/") { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "asset-object", + "field": "referenceImageAssetObjectId", + "assetObjectId": asset_object.asset_object_id, + "message": "参考图资产不是图片类型。", + })), + ); + } + if asset_object.content_length == 0 + || asset_object.content_length > PUZZLE_REFERENCE_IMAGE_MAX_BYTES as u64 + { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "asset-object", + "field": "referenceImageAssetObjectId", + "assetObjectId": asset_object.asset_object_id, + "message": "参考图资产大小不符合拼图生成要求。", + "maxBytes": PUZZLE_REFERENCE_IMAGE_MAX_BYTES, + "actualBytes": asset_object.content_length, + })), + ); + } + if let Some(expected_owner_user_id) = owner_user_id + .map(str::trim) + .filter(|value| !value.is_empty()) + { + let actual_owner_user_id = asset_object + .owner_user_id + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()); + if actual_owner_user_id != Some(expected_owner_user_id) { + return Err( + AppError::from_status(StatusCode::FORBIDDEN).with_details(json!({ + "provider": "asset-object", + "field": "referenceImageAssetObjectId", + "assetObjectId": asset_object.asset_object_id, + "message": "参考图资产不属于当前账号。", + })), + ); + } + } + + Ok(()) +} + +async fn download_signed_puzzle_reference_image( + http_client: &reqwest::Client, + signed_read_url: String, + object_key: &str, + fallback_content_type: Option<&str>, + field: &str, +) -> Result { let response = http_client - .get(signed.signed_url) + .get(signed_read_url.as_str()) .send() .await .map_err(|error| map_puzzle_image_request_error(format!("读取拼图参考图失败:{error}")))?; @@ -625,6 +855,7 @@ pub(crate) async fn resolve_puzzle_reference_image_as_data_url( .headers() .get(reqwest::header::CONTENT_TYPE) .and_then(|value| value.to_str().ok()) + .or(fallback_content_type) .unwrap_or("image/png") .to_string(); let body = response.bytes().await.map_err(|error| { @@ -636,6 +867,7 @@ pub(crate) async fn resolve_puzzle_reference_image_as_data_url( "provider": "aliyun-oss", "message": format!("读取参考图失败,状态码:{status}"), "objectKey": object_key, + "field": field, })), ); } @@ -645,6 +877,7 @@ pub(crate) async fn resolve_puzzle_reference_image_as_data_url( "provider": "aliyun-oss", "message": "读取参考图失败:对象内容为空", "objectKey": object_key, + "field": field, })), ); } @@ -655,6 +888,7 @@ pub(crate) async fn resolve_puzzle_reference_image_as_data_url( mime_type, bytes_len, bytes: body.to_vec(), + signed_read_url: Some(signed_read_url), }) } @@ -693,7 +927,7 @@ pub(crate) async fn download_puzzle_remote_image( } pub(crate) async fn persist_puzzle_generated_asset( - state: &AppState, + state: &PuzzleApiState, owner_user_id: &str, session_id: &str, level_name: &str, @@ -805,7 +1039,7 @@ pub(crate) async fn persist_puzzle_generated_asset( } pub(crate) async fn persist_puzzle_ui_background_image( - state: &AppState, + state: &PuzzleApiState, owner_user_id: &str, session_id: &str, level_name: &str, @@ -845,6 +1079,57 @@ pub(crate) async fn persist_puzzle_ui_background_image( }) } +pub(crate) async fn persist_puzzle_level_asset_image( + state: &PuzzleApiState, + owner_user_id: &str, + session_id: &str, + level_name: &str, + task_id: &str, + path_segment: &str, + asset_kind: &str, + slot: &str, + file_stem: &str, + image: PuzzleDownloadedImage, +) -> Result { + let oss_client = state.oss_client().ok_or_else(|| { + AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({ + "provider": "aliyun-oss", + "reason": "OSS 未完成环境变量配置", + })) + })?; + let http_client = reqwest::Client::new(); + let put_result = oss_client + .put_object( + &http_client, + OssPutObjectRequest { + prefix: LegacyAssetPrefix::PuzzleAssets, + path_segments: vec![ + sanitize_path_segment(session_id, "session"), + sanitize_path_segment(level_name, "puzzle"), + sanitize_path_segment(path_segment, "level-asset"), + sanitize_path_segment(task_id, "task"), + ], + file_name: format!("{file_stem}.{}", image.extension), + content_type: Some(image.mime_type.clone()), + access: OssObjectAccess::Private, + metadata: build_puzzle_level_asset_metadata( + owner_user_id, + session_id, + asset_kind, + slot, + ), + body: image.bytes, + }, + ) + .await + .map_err(map_puzzle_asset_oss_error)?; + + Ok(GeneratedPuzzleLevelAssetResponse { + image_src: put_result.legacy_public_path, + object_key: put_result.object_key, + }) +} + pub(crate) fn handle_puzzle_asset_spacetime_index_error( error: SpacetimeClientError, owner_user_id: &str, @@ -899,6 +1184,21 @@ pub(crate) fn build_puzzle_ui_background_asset_metadata( ]) } +pub(crate) fn build_puzzle_level_asset_metadata( + owner_user_id: &str, + session_id: &str, + asset_kind: &str, + slot: &str, +) -> BTreeMap { + BTreeMap::from([ + ("asset_kind".to_string(), asset_kind.to_string()), + ("owner_user_id".to_string(), owner_user_id.to_string()), + ("entity_kind".to_string(), PUZZLE_ENTITY_KIND.to_string()), + ("entity_id".to_string(), session_id.to_string()), + ("slot".to_string(), slot.to_string()), + ]) +} + pub(crate) fn parse_puzzle_json_payload( raw_text: &str, fallback_message: &str, @@ -1104,72 +1404,6 @@ pub(crate) fn map_puzzle_vector_engine_request_error(message: String) -> AppErro })) } -pub(crate) fn map_puzzle_vector_engine_reqwest_error( - context: &str, - request_url: &str, - error: reqwest::Error, -) -> AppError { - let message = format!( - "{context}:{}", - normalize_puzzle_reqwest_error_message(&error) - ); - let is_timeout = error.is_timeout() || is_puzzle_request_timeout_message(message.as_str()); - let is_connect = error.is_connect(); - let status = if is_timeout { - StatusCode::GATEWAY_TIMEOUT - } else { - StatusCode::BAD_GATEWAY - }; - let source = error.source().map(ToString::to_string).unwrap_or_default(); - - tracing::warn!( - provider = VECTOR_ENGINE_PROVIDER, - endpoint = %request_url, - timeout = is_timeout, - connect = is_connect, - request = error.is_request(), - body = error.is_body(), - source = %source, - message = %message, - "拼图 VectorEngine 请求发送失败" - ); - - AppError::from_status(status).with_details(json!({ - "provider": VECTOR_ENGINE_PROVIDER, - "message": message, - "reason": resolve_puzzle_vector_engine_request_failure_reason(&error), - "endpoint": request_url, - "timeout": is_timeout, - "connect": is_connect, - "request": error.is_request(), - "body": error.is_body(), - "source": source, - })) -} - -pub(crate) fn normalize_puzzle_reqwest_error_message(error: &reqwest::Error) -> String { - error - .to_string() - .split_whitespace() - .collect::>() - .join(" ") -} - -pub(crate) fn resolve_puzzle_vector_engine_request_failure_reason( - error: &reqwest::Error, -) -> &'static str { - if error.is_timeout() { - return "VectorEngine 图片编辑请求超时,请稍后重试或调大 VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS"; - } - if error.is_connect() { - return "无法连接 VectorEngine 图片编辑接口,请检查服务器网络、DNS、防火墙或代理配置"; - } - if error.is_body() { - return "发送 VectorEngine 图片编辑 multipart 请求体失败,请重试并检查参考图大小"; - } - "VectorEngine 图片编辑请求发送失败,请查看 source 字段中的底层网络错误" -} - pub(crate) fn is_puzzle_request_timeout_message(message: &str) -> bool { let lower = message.to_ascii_lowercase(); lower.contains("timed out") diff --git a/server-rs/crates/api-server/src/state.rs b/server-rs/crates/api-server/src/state.rs index 9249e4e5..5458e693 100644 --- a/server-rs/crates/api-server/src/state.rs +++ b/server-rs/crates/api-server/src/state.rs @@ -141,6 +141,86 @@ impl FromRef for BackpressureState { } } +#[derive(Clone, Debug)] +pub struct PuzzleApiState { + root_state: AppState, + spacetime_client: SpacetimeClient, + puzzle_gallery_cache: PuzzleGalleryCache, + oss_client: Option, + auth_user_service: AuthUserService, + llm_client: Option, + creative_agent_gpt5_client: Option, + creation_agent_llm_web_search_enabled: bool, + vector_engine_image_request_timeout_ms: u64, +} + +impl PuzzleApiState { + pub fn root_state(&self) -> &AppState { + &self.root_state + } + + pub fn spacetime_client(&self) -> &SpacetimeClient { + &self.spacetime_client + } + + pub fn puzzle_gallery_cache(&self) -> &PuzzleGalleryCache { + &self.puzzle_gallery_cache + } + + pub fn oss_client(&self) -> Option<&OssClient> { + self.oss_client.as_ref() + } + + pub fn auth_user_service(&self) -> &AuthUserService { + &self.auth_user_service + } + + pub fn llm_client(&self) -> Option<&LlmClient> { + self.llm_client.as_ref() + } + + pub fn creative_agent_gpt5_client(&self) -> Option<&LlmClient> { + self.creative_agent_gpt5_client.as_ref() + } + + pub fn creation_agent_llm_web_search_enabled(&self) -> bool { + self.creation_agent_llm_web_search_enabled + } + + pub fn vector_engine_image_request_timeout_ms(&self) -> u64 { + self.vector_engine_image_request_timeout_ms + } + + pub fn vector_engine_base_url(&self) -> &str { + self.root_state.config.vector_engine_base_url.as_str() + } + + pub fn vector_engine_api_key(&self) -> Option<&str> { + self.root_state.config.vector_engine_api_key.as_deref() + } +} + +impl FromRef for PuzzleApiState { + fn from_ref(state: &AppState) -> Self { + // 中文注释:拼图路由只暴露本能力需要的依赖快照,避免 handler 直接看见完整 AppState。 + Self { + root_state: state.clone(), + spacetime_client: state.spacetime_client.clone(), + puzzle_gallery_cache: state.puzzle_gallery_cache.clone(), + oss_client: state.oss_client.clone(), + auth_user_service: state.auth_user_service.clone(), + llm_client: state.llm_client.clone(), + creative_agent_gpt5_client: state.creative_agent_gpt5_client.clone(), + creation_agent_llm_web_search_enabled: state + .config + .creation_agent_llm_web_search_enabled, + vector_engine_image_request_timeout_ms: state + .config + .vector_engine_image_request_timeout_ms, + } + } +} + // Axum/Hyper 会在路由树和连接 service 上频繁 clone state;AppState 外层必须保持浅拷贝。 #[derive(Debug)] pub struct AppStateInner { @@ -1319,4 +1399,23 @@ mod tests { ); assert!(client.config().official_fallback()); } + + #[test] + fn puzzle_api_state_exposes_puzzle_dependency_snapshot() { + let mut config = AppConfig::default(); + config.creation_agent_llm_web_search_enabled = false; + config.vector_engine_image_request_timeout_ms = 987_654; + + let state = AppState::new(config).expect("state should build"); + let puzzle_state: PuzzleApiState = FromRef::from_ref(&state); + + assert!(!puzzle_state.creation_agent_llm_web_search_enabled()); + assert_eq!( + puzzle_state.vector_engine_image_request_timeout_ms(), + 987_654 + ); + assert!(puzzle_state.llm_client().is_none()); + assert!(puzzle_state.creative_agent_gpt5_client().is_none()); + assert!(puzzle_state.oss_client().is_none()); + } } diff --git a/server-rs/crates/api-server/src/telemetry.rs b/server-rs/crates/api-server/src/telemetry.rs index 8c217634..d4a34db4 100644 --- a/server-rs/crates/api-server/src/telemetry.rs +++ b/server-rs/crates/api-server/src/telemetry.rs @@ -172,6 +172,23 @@ pub(crate) fn update_tracking_outbox_pending_files(files: usize) { TRACKING_OUTBOX_PENDING_FILES.store(files.min(i64::MAX as usize) as i64, Ordering::Relaxed); } +pub(crate) fn record_external_api_failure( + provider: &'static str, + failure_stage: &'static str, + status_class: &'static str, + retryable: bool, +) { + external_api_metrics().failures.add( + 1, + &[ + KeyValue::new("provider", provider), + KeyValue::new("failure_stage", failure_stage), + KeyValue::new("status_class", status_class), + KeyValue::new("retryable", retryable), + ], + ); +} + fn track_response_body_in_flight(response: Response) -> Response { response.map(|body| { HTTP_RESPONSE_BODY_IN_FLIGHT.fetch_add(1, Ordering::Relaxed); @@ -211,6 +228,10 @@ struct TrackingOutboxMetrics { flushed_bytes: Counter, } +struct ExternalApiMetrics { + failures: Counter, +} + struct HttpRequestPermitsAvailableGauges { default: Arc, gallery: Arc, @@ -359,6 +380,21 @@ fn tracking_outbox_metrics() -> &'static TrackingOutboxMetrics { }) } +fn external_api_metrics() -> &'static ExternalApiMetrics { + static METRICS: std::sync::OnceLock = std::sync::OnceLock::new(); + METRICS.get_or_init(|| { + let meter = global::meter("genarrative-api"); + ExternalApiMetrics { + failures: meter + .u64_counter("genarrative.external_api.failures") + .with_description( + "External API call failures grouped by provider and failure stage", + ) + .build(), + } + }) +} + fn register_http_request_permits_available_metric() -> HttpRequestPermitsAvailableGauges { let gauges = HttpRequestPermitsAvailableGauges::new(); let meter = global::meter("genarrative-api"); diff --git a/server-rs/crates/api-server/src/tracking.rs b/server-rs/crates/api-server/src/tracking.rs index ad3b187c..82670c35 100644 --- a/server-rs/crates/api-server/src/tracking.rs +++ b/server-rs/crates/api-server/src/tracking.rs @@ -584,6 +584,26 @@ async fn record_route_tracking_event_via_outbox_after_success( record_tracking_event_input_after_success(state, request_context, event).await; } +pub(crate) fn build_tracking_event_input( + draft: TrackingEventDraft, +) -> module_runtime::RuntimeTrackingEventInput { + let occurred_at_micros = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000; + let event_id = build_tracking_event_id(&draft, occurred_at_micros); + + module_runtime::RuntimeTrackingEventInput { + event_id, + event_key: draft.event_key.to_string(), + scope_kind: draft.scope_kind, + scope_id: draft.scope_id, + user_id: draft.user_id, + owner_user_id: draft.owner_user_id, + profile_id: draft.profile_id, + module_key: draft.module_key.map(str::to_string), + metadata_json: draft.metadata.to_string(), + occurred_at_micros: occurred_at_micros as i64, + } +} + async fn record_tracking_event_input_after_success( state: &AppState, request_context: &RequestContext, @@ -642,26 +662,6 @@ async fn record_tracking_event_input_after_success( } } -fn build_tracking_event_input( - draft: TrackingEventDraft, -) -> module_runtime::RuntimeTrackingEventInput { - let occurred_at_micros = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000; - let event_id = build_tracking_event_id(&draft, occurred_at_micros); - - module_runtime::RuntimeTrackingEventInput { - event_id, - event_key: draft.event_key.to_string(), - scope_kind: draft.scope_kind, - scope_id: draft.scope_id, - user_id: draft.user_id, - owner_user_id: draft.owner_user_id, - profile_id: draft.profile_id, - module_key: draft.module_key.map(str::to_string), - metadata_json: draft.metadata.to_string(), - occurred_at_micros: occurred_at_micros as i64, - } -} - fn build_tracking_event_id(draft: &TrackingEventDraft, occurred_at_micros: i128) -> String { if draft.event_key == "daily_login" && draft.scope_kind == RuntimeTrackingScopeKind::User diff --git a/server-rs/crates/api-server/src/work_author.rs b/server-rs/crates/api-server/src/work_author.rs index e45ebfdd..38b4bea6 100644 --- a/server-rs/crates/api-server/src/work_author.rs +++ b/server-rs/crates/api-server/src/work_author.rs @@ -1,6 +1,6 @@ use module_auth::AuthUser; -use crate::state::AppState; +use crate::state::{AppState, PuzzleApiState}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct WorkAuthorSummary { @@ -14,6 +14,34 @@ pub fn resolve_work_author_by_user_id( owner_user_id: &str, fallback_display_name: Option<&str>, fallback_public_user_code: Option<&str>, +) -> WorkAuthorSummary { + resolve_work_author_by_user_id_with_service( + state.auth_user_service(), + owner_user_id, + fallback_display_name, + fallback_public_user_code, + ) +} + +pub fn resolve_puzzle_work_author_by_user_id( + state: &PuzzleApiState, + owner_user_id: &str, + fallback_display_name: Option<&str>, + fallback_public_user_code: Option<&str>, +) -> WorkAuthorSummary { + resolve_work_author_by_user_id_with_service( + state.auth_user_service(), + owner_user_id, + fallback_display_name, + fallback_public_user_code, + ) +} + +fn resolve_work_author_by_user_id_with_service( + auth_user_service: &module_auth::AuthUserService, + owner_user_id: &str, + fallback_display_name: Option<&str>, + fallback_public_user_code: Option<&str>, ) -> WorkAuthorSummary { let fallback_display_name = normalize_optional_text(fallback_display_name).unwrap_or_else(|| "玩家".to_string()); @@ -26,7 +54,7 @@ pub fn resolve_work_author_by_user_id( }; }; - match state.auth_user_service().get_user_by_id(&owner_user_id) { + match auth_user_service.get_user_by_id(&owner_user_id) { Ok(Some(user)) => map_auth_user_to_work_author_summary(user, fallback_display_name), Ok(None) | Err(_) => WorkAuthorSummary { display_name: fallback_display_name, diff --git a/server-rs/crates/api-server/src/work_play_tracking.rs b/server-rs/crates/api-server/src/work_play_tracking.rs index cad8eb56..33d722db 100644 --- a/server-rs/crates/api-server/src/work_play_tracking.rs +++ b/server-rs/crates/api-server/src/work_play_tracking.rs @@ -4,7 +4,7 @@ use serde_json::{Value, json}; use crate::{ auth::AuthenticatedAccessToken, request_context::RequestContext, - state::AppState, + state::{AppState, PuzzleApiState}, tracking::{TrackingEventDraft, record_tracking_event_after_success}, }; @@ -68,6 +68,22 @@ pub(crate) async fn record_work_play_start_after_success( state: &AppState, request_context: &RequestContext, draft: WorkPlayTrackingDraft, +) { + record_work_play_start_input_after_success(state, request_context, draft).await; +} + +pub(crate) async fn record_puzzle_work_play_start_after_success( + state: &PuzzleApiState, + request_context: &RequestContext, + draft: WorkPlayTrackingDraft, +) { + record_work_play_start_input_after_success(state.root_state(), request_context, draft).await; +} + +async fn record_work_play_start_input_after_success( + state: &AppState, + request_context: &RequestContext, + draft: WorkPlayTrackingDraft, ) { let mut metadata = json!({ "operation": WORK_PLAY_START_EVENT_KEY, diff --git a/server-rs/crates/module-auth/src/lib.rs b/server-rs/crates/module-auth/src/lib.rs index da2f52de..3d628ec1 100644 --- a/server-rs/crates/module-auth/src/lib.rs +++ b/server-rs/crates/module-auth/src/lib.rs @@ -2061,7 +2061,6 @@ fn map_sms_provider_error_to_phone_error(error: SmsProviderError) -> PhoneAuthEr SmsProviderError::InvalidConfig(message) => { PhoneAuthError::SmsProviderInvalidConfig(message) } - SmsProviderError::InvalidVerifyCode => PhoneAuthError::InvalidVerifyCode, SmsProviderError::Upstream(message) => PhoneAuthError::SmsProviderUpstream(message), } } diff --git a/server-rs/crates/module-custom-world/src/application.rs b/server-rs/crates/module-custom-world/src/application.rs index 0cf077d7..510a977c 100644 --- a/server-rs/crates/module-custom-world/src/application.rs +++ b/server-rs/crates/module-custom-world/src/application.rs @@ -544,7 +544,7 @@ pub fn build_custom_world_published_profile_compile_snapshot( let subtitle = resolve_text_field(&draft, &legacy, "subtitle").unwrap_or_default(); let summary_text = resolve_text_field(&draft, &legacy, "summary").unwrap_or_default(); let cover_image_src = resolve_cover_image_src(&draft, &legacy); - let theme_mode = resolve_theme_mode(&legacy); + let theme_mode = resolve_theme_mode(&draft, &legacy); let playable_npc_count = count_distinct_roles(draft.get("playableNpcs"), draft.get("storyNpcs")); let landmark_count = to_array(draft.get("landmarks")).len() as u32; @@ -599,6 +599,37 @@ pub fn canonicalize_custom_world_profile_before_save(profile: &mut Value) -> boo true } +pub fn resolve_custom_world_publish_setting_text( + payload: &Map, + draft_profile: &Map, + seed_text: &str, +) -> String { + // 中文注释:发布按钮的前端契约只保证提交动作名;正式 settingText 必须从草稿真相补齐, + // 避免旧会话 seed_text 为空时通过 publish gate,却在最终 compile/publish 阶段失败。 + read_nested_text_field(payload, &["settingText"]) + .or_else(|| { + read_nested_text_field( + draft_profile, + &[ + "settingText", + "creatorIntent.rawSettingText", + "creatorIntent.worldHook", + "worldHook", + "anchorContent.worldPromise", + "anchorContent.worldPromise.hook", + "summary", + "name", + "title", + ], + ) + }) + .or_else(|| { + let seed = seed_text.trim(); + (!seed.is_empty()).then(|| seed.to_string()) + }) + .unwrap_or_default() +} + pub fn empty_agent_anchor_content_json() -> String { r#"{"worldPromise":null,"playerFantasy":null,"themeBoundary":null,"playerEntryPoint":null,"coreConflict":null,"keyRelationships":null,"hiddenLines":null,"iconicElements":null}"#.to_string() } @@ -663,7 +694,13 @@ fn parse_optional_json_object( error: CustomWorldFieldError, ) -> Result, CustomWorldFieldError> { match normalize_optional_json_slice(value) { - Some(value) => parse_required_json_object(&value, error), + Some(value) => match serde_json::from_str::(&value) { + Ok(Value::Object(object)) => Ok(object), + // 中文注释:跨层可选字段经 serde 结构体序列化后可能显式落成 null; + // 对 optional JSON object 而言 null 等价于未提供,不能阻断发布链路。 + Ok(Value::Null) => Ok(Map::new()), + _ => Err(error), + }, None => Ok(Map::new()), } } @@ -804,6 +841,32 @@ fn read_text(object: &Map, key: &str) -> Option { .map(ToOwned::to_owned) } +fn read_nested_text_field(object: &Map, keys: &[&str]) -> Option { + for key in keys { + let mut current = Value::Object(object.clone()); + let mut found = true; + for segment in key.split('.') { + if let Some(next) = current.get(segment) { + current = next.clone(); + } else { + found = false; + break; + } + } + if found { + if let Some(value) = current + .as_str() + .map(str::trim) + .filter(|value| !value.is_empty()) + { + return Some(value.to_string()); + } + } + } + + None +} + fn read_string_list(object: &Map, key: &str) -> Vec { object .get(key) @@ -849,11 +912,17 @@ fn resolve_text_field( legacy: &Map, key: &str, ) -> Option { + // 中文注释:发布链路的草稿真相来自 session.draft_profile_json, + // legacyResultProfile 只补历史草稿缺失字段,不能覆盖结果页刚保存的内容。 to_text(draft.get(key)).or_else(|| to_text(legacy.get(key))) } -fn resolve_theme_mode(legacy: &Map) -> CustomWorldThemeMode { - to_text(legacy.get("themeMode")) +fn resolve_theme_mode( + draft: &Map, + legacy: &Map, +) -> CustomWorldThemeMode { + to_text(draft.get("themeMode")) + .or_else(|| to_text(legacy.get("themeMode"))) .and_then(|value| CustomWorldThemeMode::from_client_str(&value)) .unwrap_or(CustomWorldThemeMode::Mythic) } @@ -955,3 +1024,139 @@ fn build_compiled_profile_payload_json( serde_json::to_string(&Value::Object(payload)) .map_err(|_| CustomWorldFieldError::InvalidDraftProfileJson) } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn build_test_compile_input( + legacy_result_profile_json: Option, + ) -> CustomWorldPublishedProfileCompileInput { + CustomWorldPublishedProfileCompileInput { + session_id: "session-1".to_string(), + profile_id: "cwprof_001".to_string(), + owner_user_id: "user-1".to_string(), + draft_profile_json: json!({ + "name": "潮雾列岛", + "summary": "群岛与旧灯塔之间的沉船疑案。", + "playableNpcs": [], + "storyNpcs": [], + "landmarks": [] + }) + .to_string(), + legacy_result_profile_json, + setting_text: "海图会在午夜改写群岛航路。".to_string(), + author_display_name: "创作者".to_string(), + updated_at_micros: 1, + } + } + + #[test] + fn published_profile_compile_treats_null_legacy_result_profile_as_absent() { + let snapshot = build_custom_world_published_profile_compile_snapshot( + build_test_compile_input(Some("null".to_string())), + ) + .expect("null legacy result profile should be treated as absent"); + + assert_eq!(snapshot.profile_id, "cwprof_001"); + assert_eq!(snapshot.world_name, "潮雾列岛"); + } + + #[test] + fn published_profile_compile_rejects_non_object_legacy_result_profile() { + let error = build_custom_world_published_profile_compile_snapshot( + build_test_compile_input(Some("[]".to_string())), + ) + .expect_err("array legacy result profile should still be invalid"); + + assert_eq!(error, CustomWorldFieldError::InvalidLegacyResultProfileJson); + } + + #[test] + fn published_profile_compile_prefers_saved_draft_over_legacy_profile() { + let input = CustomWorldPublishedProfileCompileInput { + draft_profile_json: json!({ + "name": "结果页保存后的世界", + "summary": "发布前最后一次填写的摘要。", + "themeMode": "tide", + "playableNpcs": [], + "storyNpcs": [], + "landmarks": [] + }) + .to_string(), + legacy_result_profile_json: Some( + json!({ + "name": "旧结果页世界", + "summary": "旧摘要不应覆盖保存草稿。", + "themeMode": "mythic" + }) + .to_string(), + ), + ..build_test_compile_input(None) + }; + + let snapshot = build_custom_world_published_profile_compile_snapshot(input) + .expect("compile should prefer saved draft"); + let payload: Value = serde_json::from_str(&snapshot.compiled_profile_payload_json) + .expect("compiled payload should be json"); + + assert_eq!(snapshot.world_name, "结果页保存后的世界"); + assert_eq!(snapshot.summary_text, "发布前最后一次填写的摘要。"); + assert_eq!(snapshot.theme_mode, CustomWorldThemeMode::Tide); + assert_eq!( + payload.get("name").and_then(Value::as_str), + Some("结果页保存后的世界") + ); + assert_eq!( + payload.get("summary").and_then(Value::as_str), + Some("发布前最后一次填写的摘要。") + ); + } + + #[test] + fn publish_setting_text_falls_back_to_draft_profile_when_seed_is_empty() { + let payload = Map::new(); + let draft_profile = json!({ + "settingText": "海雾会吞掉记错航线的人。", + "worldHook": "在失真的海图上追查一场被篡改的沉船事故。", + "summary": "守灯人与群岛议会围绕沉船旧案对峙。" + }) + .as_object() + .cloned() + .expect("draft profile should be object"); + + let setting_text = resolve_custom_world_publish_setting_text(&payload, &draft_profile, ""); + + assert_eq!(setting_text, "海雾会吞掉记错航线的人。"); + } + + #[test] + fn publish_setting_text_prefers_payload_then_draft_then_seed() { + let mut payload = Map::new(); + payload.insert( + "settingText".to_string(), + Value::String("发布载荷设定".to_string()), + ); + let draft_profile = json!({ + "worldHook": "草稿世界一句话", + "summary": "草稿摘要" + }) + .as_object() + .cloned() + .expect("draft profile should be object"); + + assert_eq!( + resolve_custom_world_publish_setting_text(&payload, &draft_profile, "用户原始设定"), + "发布载荷设定" + ); + assert_eq!( + resolve_custom_world_publish_setting_text(&Map::new(), &draft_profile, "用户原始设定"), + "草稿世界一句话" + ); + assert_eq!( + resolve_custom_world_publish_setting_text(&Map::new(), &Map::new(), "用户原始设定"), + "用户原始设定" + ); + } +} diff --git a/server-rs/crates/module-jump-hop/Cargo.toml b/server-rs/crates/module-jump-hop/Cargo.toml new file mode 100644 index 00000000..1ca24ddf --- /dev/null +++ b/server-rs/crates/module-jump-hop/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "module-jump-hop" +edition.workspace = true +version.workspace = true +license.workspace = true + +[features] +default = [] +spacetime-types = ["dep:spacetimedb"] + +[dependencies] +serde = { workspace = true } +shared-kernel = { workspace = true } +spacetimedb = { workspace = true, optional = true } diff --git a/server-rs/crates/module-jump-hop/src/application.rs b/server-rs/crates/module-jump-hop/src/application.rs new file mode 100644 index 00000000..e7f835bf --- /dev/null +++ b/server-rs/crates/module-jump-hop/src/application.rs @@ -0,0 +1,395 @@ +use shared_kernel::normalize_required_string; + +use crate::{ + JumpHopDifficulty, JumpHopError, JumpHopJumpResultKind, JumpHopLastJump, JumpHopPath, + JumpHopPlatform, JumpHopRunSnapshot, JumpHopRunStatus, JumpHopScoring, JumpHopTileType, +}; + +pub fn generate_jump_hop_path(seed: &str, difficulty: JumpHopDifficulty) -> JumpHopPath { + let config = difficulty_config(difficulty); + let mut rng = DeterministicRng::new(seed, difficulty.as_str()); + let platform_count = rng.range_u32(config.min_platforms, config.max_platforms) as usize; + let mut platforms = Vec::with_capacity(platform_count); + let mut x = 0.0f32; + let mut y = 0.0f32; + + for index in 0..platform_count { + let tile_type = if index == 0 { + JumpHopTileType::Start + } else if index + 1 == platform_count { + JumpHopTileType::Finish + } else if index % 7 == 0 { + JumpHopTileType::Bonus + } else if index % 5 == 0 { + JumpHopTileType::Target + } else if index % 4 == 0 { + JumpHopTileType::Accent + } else { + JumpHopTileType::Normal + }; + let width = rng.range_f32(config.min_width, config.max_width); + let height = width * rng.range_f32(0.86, 1.04); + let landing_radius = width * config.landing_radius_factor; + let perfect_radius = landing_radius * config.perfect_radius_factor; + + platforms.push(JumpHopPlatform { + platform_id: format!("jump-hop-platform-{index:03}"), + tile_type, + x, + y, + width, + height, + landing_radius, + perfect_radius, + score_value: if tile_type == JumpHopTileType::Bonus { + 180 + } else { + 100 + }, + }); + + if index + 1 < platform_count { + let distance = rng.range_f32(config.min_gap, config.max_gap); + let direction = if rng.next_u32() % 2 == 0 { 1.0 } else { -1.0 }; + x += distance * 0.62 * direction; + y += distance; + } + } + + JumpHopPath { + seed: seed.trim().to_string(), + difficulty, + finish_index: platform_count.saturating_sub(1) as u32, + platforms, + camera_preset: "portrait-isometric-9x16".to_string(), + scoring: JumpHopScoring { + charge_to_distance_ratio: config.charge_to_distance_ratio, + max_charge_ms: config.max_charge_ms, + hit_bonus: 20, + perfect_bonus: 60, + }, + } +} + +pub fn start_run( + run_id: String, + owner_user_id: String, + profile_id: String, + path: JumpHopPath, + started_at_ms: u64, +) -> Result { + let run_id = normalize_required_string(run_id).ok_or(JumpHopError::MissingRunId)?; + let owner_user_id = + normalize_required_string(owner_user_id).ok_or(JumpHopError::MissingOwnerUserId)?; + let profile_id = normalize_required_string(profile_id).ok_or(JumpHopError::MissingProfileId)?; + if path.platforms.is_empty() { + return Err(JumpHopError::EmptyPath); + } + + Ok(JumpHopRunSnapshot { + run_id, + profile_id, + owner_user_id, + status: JumpHopRunStatus::Playing, + current_platform_index: 0, + score: 0, + combo: 0, + last_jump: None, + started_at_ms, + finished_at_ms: None, + path, + }) +} + +pub fn apply_jump( + run: &JumpHopRunSnapshot, + charge_ms: u32, + jumped_at_ms: u64, +) -> Result { + if run.status != JumpHopRunStatus::Playing { + return Err(JumpHopError::RunNotPlaying); + } + let current_index = run.current_platform_index as usize; + let next_index = current_index + 1; + let current = run + .path + .platforms + .get(current_index) + .ok_or(JumpHopError::EmptyPath)?; + let target = run + .path + .platforms + .get(next_index) + .ok_or(JumpHopError::NoNextPlatform)?; + let capped_charge = charge_ms.min(run.path.scoring.max_charge_ms); + let jump_distance = capped_charge as f32 * run.path.scoring.charge_to_distance_ratio; + let vector_x = target.x - current.x; + let vector_y = target.y - current.y; + let target_distance = vector_x.hypot(vector_y).max(0.0001); + let unit_x = vector_x / target_distance; + let unit_y = vector_y / target_distance; + let landed_x = current.x + unit_x * jump_distance; + let landed_y = current.y + unit_y * jump_distance; + let landing_error = (landed_x - target.x).hypot(landed_y - target.y); + + let mut next = run.clone(); + let result = if landing_error <= target.perfect_radius { + if next_index as u32 == run.path.finish_index { + JumpHopJumpResultKind::Finish + } else { + JumpHopJumpResultKind::Perfect + } + } else if landing_error <= target.landing_radius { + if next_index as u32 == run.path.finish_index { + JumpHopJumpResultKind::Finish + } else { + JumpHopJumpResultKind::Hit + } + } else { + JumpHopJumpResultKind::Miss + }; + + next.last_jump = Some(JumpHopLastJump { + charge_ms: capped_charge, + jump_distance, + target_platform_index: next_index as u32, + landed_x, + landed_y, + result, + }); + + if result == JumpHopJumpResultKind::Miss { + next.status = JumpHopRunStatus::Failed; + next.combo = 0; + next.finished_at_ms = Some(jumped_at_ms); + return Ok(next); + } + + next.current_platform_index = next_index as u32; + next.combo = next.combo.saturating_add(1); + next.score = next.score.saturating_add(target.score_value); + if matches!( + result, + JumpHopJumpResultKind::Perfect | JumpHopJumpResultKind::Finish + ) { + next.score = next + .score + .saturating_add(run.path.scoring.perfect_bonus) + .saturating_add(next.combo.saturating_mul(run.path.scoring.hit_bonus)); + } else { + next.score = next.score.saturating_add(run.path.scoring.hit_bonus); + } + if result == JumpHopJumpResultKind::Finish { + next.status = JumpHopRunStatus::Cleared; + next.finished_at_ms = Some(jumped_at_ms); + } + + Ok(next) +} + +pub fn restart_run( + run: &JumpHopRunSnapshot, + next_run_id: String, + restarted_at_ms: u64, +) -> Result { + start_run( + next_run_id, + run.owner_user_id.clone(), + run.profile_id.clone(), + run.path.clone(), + restarted_at_ms, + ) +} + +struct DifficultyConfig { + min_platforms: u32, + max_platforms: u32, + min_gap: f32, + max_gap: f32, + min_width: f32, + max_width: f32, + landing_radius_factor: f32, + perfect_radius_factor: f32, + charge_to_distance_ratio: f32, + max_charge_ms: u32, +} + +fn difficulty_config(difficulty: JumpHopDifficulty) -> DifficultyConfig { + match difficulty { + JumpHopDifficulty::Easy => DifficultyConfig { + min_platforms: 12, + max_platforms: 14, + min_gap: 1.0, + max_gap: 1.45, + min_width: 0.9, + max_width: 1.08, + landing_radius_factor: 0.62, + perfect_radius_factor: 0.32, + charge_to_distance_ratio: 0.004, + max_charge_ms: 700, + }, + JumpHopDifficulty::Standard => DifficultyConfig { + min_platforms: 16, + max_platforms: 18, + min_gap: 1.22, + max_gap: 1.78, + min_width: 0.82, + max_width: 1.0, + landing_radius_factor: 0.54, + perfect_radius_factor: 0.26, + charge_to_distance_ratio: 0.004, + max_charge_ms: 780, + }, + JumpHopDifficulty::Advanced => DifficultyConfig { + min_platforms: 20, + max_platforms: 24, + min_gap: 1.45, + max_gap: 2.05, + min_width: 0.72, + max_width: 0.94, + landing_radius_factor: 0.48, + perfect_radius_factor: 0.22, + charge_to_distance_ratio: 0.004, + max_charge_ms: 860, + }, + JumpHopDifficulty::Challenge => DifficultyConfig { + min_platforms: 26, + max_platforms: 32, + min_gap: 1.7, + max_gap: 2.35, + min_width: 0.66, + max_width: 0.88, + landing_radius_factor: 0.42, + perfect_radius_factor: 0.18, + charge_to_distance_ratio: 0.004, + max_charge_ms: 950, + }, + } +} + +struct DeterministicRng { + state: u64, +} + +impl DeterministicRng { + fn new(seed: &str, salt: &str) -> Self { + let mut state = 0xcbf2_9ce4_8422_2325u64; + for byte in seed.bytes().chain(salt.bytes()) { + state ^= u64::from(byte); + state = state.wrapping_mul(0x1000_0000_01b3); + } + Self { state } + } + + fn next_u32(&mut self) -> u32 { + self.state = self + .state + .wrapping_mul(6_364_136_223_846_793_005) + .wrapping_add(1); + (self.state >> 32) as u32 + } + + fn range_u32(&mut self, min: u32, max: u32) -> u32 { + if max <= min { + return min; + } + min + self.next_u32() % (max - min + 1) + } + + fn range_f32(&mut self, min: f32, max: f32) -> f32 { + if max <= min { + return min; + } + let unit = self.next_u32() as f32 / u32::MAX as f32; + min + (max - min) * unit + } +} + +#[cfg(test)] +mod tests { + use crate::{ + JumpHopDifficulty, JumpHopJumpResultKind, JumpHopRunStatus, apply_jump, + generate_jump_hop_path, restart_run, start_run, + }; + + #[test] + fn path_generation_is_seeded_and_uses_difficulty_ranges() { + let first = generate_jump_hop_path("seed-a", JumpHopDifficulty::Standard); + let second = generate_jump_hop_path("seed-a", JumpHopDifficulty::Standard); + let challenge = generate_jump_hop_path("seed-a", JumpHopDifficulty::Challenge); + + assert_eq!(first, second); + assert!((16..=18).contains(&first.platforms.len())); + assert!((26..=32).contains(&challenge.platforms.len())); + assert_eq!(first.platforms.first().unwrap().tile_type.as_str(), "start"); + assert_eq!(first.platforms.last().unwrap().tile_type.as_str(), "finish"); + } + + #[test] + fn jump_resolution_distinguishes_perfect_hit_and_miss() { + let path = generate_jump_hop_path("seed-b", JumpHopDifficulty::Easy); + let run = start_run( + "run-1".to_string(), + "user-1".to_string(), + "profile-1".to_string(), + path, + 100, + ) + .expect("run should start"); + let target = &run.path.platforms[1]; + let distance = target.x.hypot(target.y); + let perfect_charge = (distance / run.path.scoring.charge_to_distance_ratio) as u32; + + let perfect = apply_jump(&run, perfect_charge, 200).expect("jump should resolve"); + assert_eq!( + perfect.last_jump.as_ref().unwrap().result, + JumpHopJumpResultKind::Perfect + ); + assert_eq!(perfect.status, JumpHopRunStatus::Playing); + assert_eq!(perfect.current_platform_index, 1); + + let hit = + apply_jump(&run, perfect_charge.saturating_add(80), 200).expect("jump should resolve"); + assert_eq!( + hit.last_jump.as_ref().unwrap().result, + JumpHopJumpResultKind::Hit + ); + + let miss = + apply_jump(&run, perfect_charge.saturating_add(900), 200).expect("jump should resolve"); + assert_eq!(miss.status, JumpHopRunStatus::Failed); + assert_eq!( + miss.last_jump.as_ref().unwrap().result, + JumpHopJumpResultKind::Miss + ); + } + + #[test] + fn restart_returns_to_first_platform_and_playing_state() { + let path = generate_jump_hop_path("seed-c", JumpHopDifficulty::Easy); + let mut run = start_run( + "run-1".to_string(), + "user-1".to_string(), + "profile-1".to_string(), + path, + 100, + ) + .expect("run should start"); + run.status = JumpHopRunStatus::Failed; + run.current_platform_index = 3; + run.score = 300; + run.combo = 2; + run.finished_at_ms = Some(200); + + let restarted = restart_run(&run, "run-2".to_string(), 300).expect("run should restart"); + + assert_eq!(restarted.run_id, "run-2"); + assert_eq!(restarted.status, JumpHopRunStatus::Playing); + assert_eq!(restarted.current_platform_index, 0); + assert_eq!(restarted.score, 0); + assert_eq!(restarted.combo, 0); + assert!(restarted.last_jump.is_none()); + assert_eq!(restarted.started_at_ms, 300); + assert!(restarted.finished_at_ms.is_none()); + } +} diff --git a/server-rs/crates/module-jump-hop/src/commands.rs b/server-rs/crates/module-jump-hop/src/commands.rs new file mode 100644 index 00000000..7da94e8c --- /dev/null +++ b/server-rs/crates/module-jump-hop/src/commands.rs @@ -0,0 +1,18 @@ +use shared_kernel::normalize_required_string; + +use crate::JumpHopDifficulty; + +pub fn parse_jump_hop_difficulty(value: &str) -> JumpHopDifficulty { + match value.trim().to_ascii_lowercase().as_str() { + "easy" | "轻松" => JumpHopDifficulty::Easy, + "advanced" | "进阶" => JumpHopDifficulty::Advanced, + "challenge" | "挑战" => JumpHopDifficulty::Challenge, + _ => JumpHopDifficulty::Standard, + } +} + +pub fn normalize_jump_hop_seed(seed: &str, fallback: &str) -> String { + normalize_required_string(seed) + .or_else(|| normalize_required_string(fallback)) + .unwrap_or_else(|| "jump-hop".to_string()) +} diff --git a/server-rs/crates/module-jump-hop/src/domain.rs b/server-rs/crates/module-jump-hop/src/domain.rs new file mode 100644 index 00000000..bfc20e7f --- /dev/null +++ b/server-rs/crates/module-jump-hop/src/domain.rs @@ -0,0 +1,151 @@ +use serde::{Deserialize, Serialize}; +#[cfg(feature = "spacetime-types")] +use spacetimedb::SpacetimeType; + +pub const JUMP_HOP_SESSION_ID_PREFIX: &str = "jump-hop-session-"; +pub const JUMP_HOP_PROFILE_ID_PREFIX: &str = "jump-hop-profile-"; +pub const JUMP_HOP_WORK_ID_PREFIX: &str = "jump-hop-work-"; +pub const JUMP_HOP_RUN_ID_PREFIX: &str = "jump-hop-run-"; + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum JumpHopDifficulty { + Easy, + Standard, + Advanced, + Challenge, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum JumpHopTileType { + Start, + Normal, + Target, + Finish, + Bonus, + Accent, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum JumpHopRunStatus { + Playing, + Failed, + Cleared, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum JumpHopJumpResultKind { + Miss, + Hit, + Perfect, + Finish, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct JumpHopScoring { + pub charge_to_distance_ratio: f32, + pub max_charge_ms: u32, + pub hit_bonus: u32, + pub perfect_bonus: u32, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct JumpHopPlatform { + pub platform_id: String, + pub tile_type: JumpHopTileType, + pub x: f32, + pub y: f32, + pub width: f32, + pub height: f32, + pub landing_radius: f32, + pub perfect_radius: f32, + pub score_value: u32, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct JumpHopPath { + pub seed: String, + pub difficulty: JumpHopDifficulty, + pub platforms: Vec, + pub finish_index: u32, + pub camera_preset: String, + pub scoring: JumpHopScoring, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct JumpHopLastJump { + pub charge_ms: u32, + pub jump_distance: f32, + pub target_platform_index: u32, + pub landed_x: f32, + pub landed_y: f32, + pub result: JumpHopJumpResultKind, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct JumpHopRunSnapshot { + pub run_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub status: JumpHopRunStatus, + pub current_platform_index: u32, + pub score: u32, + pub combo: u32, + pub last_jump: Option, + pub started_at_ms: u64, + pub finished_at_ms: Option, + pub path: JumpHopPath, +} + +impl JumpHopDifficulty { + pub fn as_str(self) -> &'static str { + match self { + Self::Easy => "easy", + Self::Standard => "standard", + Self::Advanced => "advanced", + Self::Challenge => "challenge", + } + } +} + +impl JumpHopTileType { + pub fn as_str(self) -> &'static str { + match self { + Self::Start => "start", + Self::Normal => "normal", + Self::Target => "target", + Self::Finish => "finish", + Self::Bonus => "bonus", + Self::Accent => "accent", + } + } +} + +impl JumpHopRunStatus { + pub fn as_str(self) -> &'static str { + match self { + Self::Playing => "playing", + Self::Failed => "failed", + Self::Cleared => "cleared", + } + } +} + +impl JumpHopJumpResultKind { + pub fn as_str(self) -> &'static str { + match self { + Self::Miss => "miss", + Self::Hit => "hit", + Self::Perfect => "perfect", + Self::Finish => "finish", + } + } +} diff --git a/server-rs/crates/module-jump-hop/src/errors.rs b/server-rs/crates/module-jump-hop/src/errors.rs new file mode 100644 index 00000000..6ce73bf5 --- /dev/null +++ b/server-rs/crates/module-jump-hop/src/errors.rs @@ -0,0 +1,27 @@ +use std::fmt::{self, Display}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum JumpHopError { + MissingRunId, + MissingProfileId, + MissingOwnerUserId, + EmptyPath, + RunNotPlaying, + NoNextPlatform, +} + +impl Display for JumpHopError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = match self { + Self::MissingRunId => "缺少 runId", + Self::MissingProfileId => "缺少 profileId", + Self::MissingOwnerUserId => "owner_user_id 缺失", + Self::EmptyPath => "跳一跳路径为空", + Self::RunNotPlaying => "当前运行态不是 playing", + Self::NoNextPlatform => "没有下一块平台", + }; + write!(f, "{message}") + } +} + +impl std::error::Error for JumpHopError {} diff --git a/server-rs/crates/module-jump-hop/src/events.rs b/server-rs/crates/module-jump-hop/src/events.rs new file mode 100644 index 00000000..70179dcd --- /dev/null +++ b/server-rs/crates/module-jump-hop/src/events.rs @@ -0,0 +1,23 @@ +//! 跳一跳领域事件。 +//! +//! 事件只表达已发生的领域事实,是否持久化、投影或广播由 SpacetimeDB adapter 决定。 + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum JumpHopDomainEvent { + DraftCompiled { + profile_id: String, + owner_user_id: String, + occurred_at_micros: i64, + }, + WorkPublished { + profile_id: String, + owner_user_id: String, + occurred_at_micros: i64, + }, + RunSettled { + run_id: String, + owner_user_id: String, + status: String, + occurred_at_micros: i64, + }, +} diff --git a/server-rs/crates/module-jump-hop/src/lib.rs b/server-rs/crates/module-jump-hop/src/lib.rs new file mode 100644 index 00000000..6acdc7c7 --- /dev/null +++ b/server-rs/crates/module-jump-hop/src/lib.rs @@ -0,0 +1,11 @@ +mod application; +mod commands; +mod domain; +mod errors; +mod events; + +pub use application::*; +pub use commands::*; +pub use domain::*; +pub use errors::*; +pub use events::*; diff --git a/server-rs/crates/module-match3d/src/application.rs b/server-rs/crates/module-match3d/src/application.rs index 64ddef75..fa4db934 100644 --- a/server-rs/crates/module-match3d/src/application.rs +++ b/server-rs/crates/module-match3d/src/application.rs @@ -419,12 +419,12 @@ pub fn resolve_match3d_item_type_count_for_difficulty(clear_count: u32, difficul 8 => 3, 12 => 9, 16 => 15, - 20 | 21 => 21, + 20 | 21 => 20, _ => match difficulty { 0..=2 => 3, 3..=4 => 9, 5..=6 => 15, - _ => 21, + _ => 20, }, }; @@ -432,8 +432,8 @@ pub fn resolve_match3d_item_type_count_for_difficulty(clear_count: u32, difficul } pub fn normalize_match3d_runtime_clear_count(clear_count: u32, difficulty: u32) -> u32 { - // 中文注释:旧硬核草稿曾保存 clear_count=20;新硬核固定 21 种物品, - // 运行态也升到 21 组三消,避免出现 20 组却要求 21 种素材的不可达状态。 + // 中文注释:旧硬核草稿曾保存 clear_count=20;运行态保留硬核 21 组三消节奏, + // 但本局物品类型池仍由难度映射到最多 20 种,避免超过 10*10 Sprite 解析素材上限。 if clear_count == 20 && difficulty >= 7 { 21 } else { @@ -885,7 +885,7 @@ mod tests { } #[test] - fn legacy_hardcore_clear_count_runs_as_twenty_one_groups() { + fn legacy_hardcore_clear_count_runs_with_twenty_item_types() { let run = start_run_with_seed_at( "run-types-legacy-hardcore".to_string(), "user-1".to_string(), @@ -903,8 +903,8 @@ mod tests { assert_eq!(run.clear_count, 21); assert_eq!(run.total_item_count, 63); - assert_eq!(counts.len(), 21); - assert!(counts.values().all(|count| *count == 3)); + assert_eq!(counts.len(), 20); + assert_eq!(counts.values().sum::(), 63); } #[test] @@ -931,8 +931,8 @@ mod tests { } #[test] - fn size_tier_plan_follows_ratio_for_twenty_five_types() { - let plan = resolve_size_tier_plan(25); + fn size_tier_plan_follows_ratio_for_twenty_types() { + let plan = resolve_size_tier_plan(20); let mut counts = BTreeMap::<&str, usize>::new(); for rule in plan { *counts.entry(rule.tier).or_default() += 1; @@ -946,10 +946,10 @@ mod tests { } } - assert_eq!(counts.get("XL"), Some(&5)); - assert_eq!(counts.get("L"), Some(&8)); - assert_eq!(counts.get("M"), Some(&7)); - assert_eq!(counts.get("XS"), Some(&4)); + assert_eq!(counts.get("XL"), Some(&4)); + assert_eq!(counts.get("L"), Some(&6)); + assert_eq!(counts.get("M"), Some(&6)); + assert_eq!(counts.get("XS"), Some(&3)); assert_eq!(counts.get("S"), Some(&1)); } @@ -962,7 +962,7 @@ mod tests { &test_config(30), 42, 1_000, - Some(25), + Some(20), ) .expect("run should start"); @@ -974,7 +974,7 @@ mod tests { .push((item.radius * 10_000.0).round() as u32); } - assert_eq!(radii_by_visual_key.len(), 25); + assert_eq!(radii_by_visual_key.len(), 20); assert!( radii_by_visual_key .values() @@ -1052,7 +1052,7 @@ mod tests { .filter(|item| { let dx = item.x - MATCH3D_BOARD_CENTER; let dy = item.y - MATCH3D_BOARD_CENTER; - (dx * dx + dy * dy).sqrt() > 0.32 + (dx * dx + dy * dy).sqrt() > 0.26 }) .count(); let mut quadrants = BTreeMap::::new(); @@ -1081,15 +1081,15 @@ mod tests { } #[test] - fn twenty_five_or_less_does_not_repeat_visual_keys() { + fn twenty_or_less_does_not_repeat_visual_keys() { let run = start_run_with_seed_at_and_item_type_count( "run-block-unique".to_string(), "user-1".to_string(), "profile-1".to_string(), - &test_config(25), + &test_config(20), 27, 1_000, - Some(25), + Some(20), ) .expect("run should start"); @@ -1098,7 +1098,7 @@ mod tests { *counts.entry(item.visual_key.clone()).or_default() += 1; } - assert_eq!(counts.len(), 25); + assert_eq!(counts.len(), 20); assert!(counts.values().all(|count| *count == 3)); } diff --git a/server-rs/crates/module-match3d/src/domain.rs b/server-rs/crates/module-match3d/src/domain.rs index 53e50b08..e25f7c47 100644 --- a/server-rs/crates/module-match3d/src/domain.rs +++ b/server-rs/crates/module-match3d/src/domain.rs @@ -9,8 +9,8 @@ pub const MATCH3D_WORK_ID_PREFIX: &str = "match3d-work-"; pub const MATCH3D_RUN_ID_PREFIX: &str = "match3d-run-"; pub const MATCH3D_TRAY_SLOT_COUNT: u32 = 7; pub const MATCH3D_ITEMS_PER_CLEAR: u32 = 3; -pub const MATCH3D_MAX_ITEM_TYPE_COUNT: u32 = 25; -pub(crate) const MATCH3D_MAX_ITEM_TYPE_COUNT_USIZE: usize = 25; +pub const MATCH3D_MAX_ITEM_TYPE_COUNT: u32 = 20; +pub(crate) const MATCH3D_MAX_ITEM_TYPE_COUNT_USIZE: usize = 20; pub const MATCH3D_MIN_DIFFICULTY: u32 = 1; pub const MATCH3D_MAX_DIFFICULTY: u32 = 10; pub const MATCH3D_DEFAULT_DURATION_LIMIT_MS: u64 = 10 * 60 * 1000; @@ -18,8 +18,7 @@ pub const MATCH3D_BOARD_CENTER: f32 = 0.5; pub const MATCH3D_BOARD_RADIUS: f32 = 0.5; pub const MATCH3D_BOARD_SAFE_MARGIN: f32 = 0.035; -// 中文注释:首版 demo 不接真实图片生成,当前先用程序化积木件作为稳定可辨认的默认素材。 -// 中文注释:当前 demo 使用 25 个积木件作为默认可消除物资源池,前端据 visual_key 程序化生成 3D 模型。 +// 中文注释:默认资源池对齐抓大鹅 10*10 物品 Sprite 的 20 种物品上限。 pub(crate) const MATCH3D_BLOCK_VISUAL_KEYS: [&str; MATCH3D_MAX_ITEM_TYPE_COUNT_USIZE] = [ "block-red-2x4", "block-blue-1x2", @@ -29,19 +28,14 @@ pub(crate) const MATCH3D_BLOCK_VISUAL_KEYS: [&str; MATCH3D_MAX_ITEM_TYPE_COUNT_U "block-white-1x1", "block-black-1x8", "block-tan-2x3", - "block-lime-1x2", "block-darkred-2x2", "block-blue-1x4", "block-pink-2x4", "block-gray-1x6", "block-lavender-tile-2x2", "block-teal-tile-1x3", - "block-mint-tile-1x4", - "block-magenta-tile-2x2", "block-orange-tile-2x2-stud", "block-purple-slope-1x2", - "block-brown-slope-1x2", - "block-sky-slope-2x2", "block-green-cylinder", "block-clear-ring", "block-mint-arch", diff --git a/server-rs/crates/module-puzzle/src/application.rs b/server-rs/crates/module-puzzle/src/application.rs index eed25933..a3cdfa8b 100644 --- a/server-rs/crates/module-puzzle/src/application.rs +++ b/server-rs/crates/module-puzzle/src/application.rs @@ -189,6 +189,12 @@ pub fn compile_result_draft_from_seed( ui_background_prompt: None, ui_background_image_src: None, ui_background_image_object_key: None, + level_scene_image_src: None, + level_scene_image_object_key: None, + ui_spritesheet_image_src: None, + ui_spritesheet_image_object_key: None, + level_background_image_src: None, + level_background_image_object_key: None, background_music: None, candidates: Vec::new(), selected_candidate_id: None, @@ -249,6 +255,12 @@ pub fn build_form_draft_from_parts( ui_background_prompt: None, ui_background_image_src: None, ui_background_image_object_key: None, + level_scene_image_src: None, + level_scene_image_object_key: None, + ui_spritesheet_image_src: None, + ui_spritesheet_image_object_key: None, + level_background_image_src: None, + level_background_image_object_key: None, background_music: None, candidates: Vec::new(), selected_candidate_id: None, @@ -358,6 +370,12 @@ pub fn normalize_puzzle_draft(mut draft: PuzzleResultDraft) -> PuzzleResultDraft ui_background_prompt: None, ui_background_image_src: None, ui_background_image_object_key: None, + level_scene_image_src: None, + level_scene_image_object_key: None, + ui_spritesheet_image_src: None, + ui_spritesheet_image_object_key: None, + level_background_image_src: None, + level_background_image_object_key: None, background_music: None, candidates: draft.candidates.clone(), selected_candidate_id: draft.selected_candidate_id.clone(), @@ -448,6 +466,12 @@ pub fn append_blank_puzzle_level(draft: &PuzzleResultDraft) -> PuzzleResultDraft ui_background_prompt: None, ui_background_image_src: None, ui_background_image_object_key: None, + level_scene_image_src: None, + level_scene_image_object_key: None, + ui_spritesheet_image_src: None, + ui_spritesheet_image_object_key: None, + level_background_image_src: None, + level_background_image_object_key: None, background_music: None, candidates: Vec::new(), selected_candidate_id: None, @@ -804,17 +828,89 @@ fn first_profile_ui_background_level(profile: &PuzzleWorkProfile) -> Option Option { + normalize_puzzle_levels(profile.levels.clone(), &profile.theme_tags) + .unwrap_or_else(|_| profile.levels.clone()) + .into_iter() + .find(|level| { + level + .level_background_image_src + .as_deref() + .and_then(normalize_required_string) + .is_some() + || level + .level_background_image_object_key + .as_deref() + .and_then(normalize_required_string) + .is_some() + }) +} + fn resolve_puzzle_runtime_ui_background_fields( level: Option<&PuzzleDraftLevel>, fallback_level: Option<&PuzzleDraftLevel>, ) -> (Option, Option) { for candidate in [level, fallback_level].into_iter().flatten() { let image_src = candidate - .ui_background_image_src + .level_background_image_src + .as_deref() + .and_then(normalize_required_string) + .or_else(|| { + candidate + .ui_background_image_src + .as_deref() + .and_then(normalize_required_string) + }); + let object_key = candidate + .level_background_image_object_key + .as_deref() + .and_then(|value| normalize_required_string(value.trim_start_matches('/'))) + .or_else(|| { + candidate + .ui_background_image_object_key + .as_deref() + .and_then(|value| normalize_required_string(value.trim_start_matches('/'))) + }); + if image_src.is_some() || object_key.is_some() { + return (image_src, object_key); + } + } + + (None, None) +} + +fn resolve_puzzle_runtime_level_background_fields( + level: Option<&PuzzleDraftLevel>, + fallback_level: Option<&PuzzleDraftLevel>, +) -> (Option, Option) { + for candidate in [level, fallback_level].into_iter().flatten() { + let image_src = candidate + .level_background_image_src .as_deref() .and_then(normalize_required_string); let object_key = candidate - .ui_background_image_object_key + .level_background_image_object_key + .as_deref() + .and_then(|value| normalize_required_string(value.trim_start_matches('/'))); + if image_src.is_some() || object_key.is_some() { + return (image_src, object_key); + } + } + + (None, None) +} + +fn resolve_puzzle_runtime_ui_spritesheet_fields( + level: Option<&PuzzleDraftLevel>, + fallback_level: Option<&PuzzleDraftLevel>, +) -> (Option, Option) { + for candidate in [level, fallback_level].into_iter().flatten() { + let image_src = candidate + .ui_spritesheet_image_src + .as_deref() + .and_then(normalize_required_string); + let object_key = candidate + .ui_spritesheet_image_object_key .as_deref() .and_then(|value| normalize_required_string(value.trim_start_matches('/'))); if image_src.is_some() || object_key.is_some() { @@ -1092,6 +1188,17 @@ pub fn start_run_with_shuffle_seed_at( current_profile_level.as_ref(), ui_background_level.as_ref(), ); + let level_background_level = first_profile_level_background_level(entry_profile); + let (level_background_image_src, level_background_image_object_key) = + resolve_puzzle_runtime_level_background_fields( + current_profile_level.as_ref(), + level_background_level.as_ref(), + ); + let (ui_spritesheet_image_src, ui_spritesheet_image_object_key) = + resolve_puzzle_runtime_ui_spritesheet_fields( + current_profile_level.as_ref(), + entry_profile.levels.first(), + ); Ok(PuzzleRunSnapshot { run_id: run_id.clone(), entry_profile_id: entry_profile.profile_id.clone(), @@ -1114,6 +1221,10 @@ pub fn start_run_with_shuffle_seed_at( cover_image_src: entry_profile.cover_image_src.clone(), ui_background_image_src, ui_background_image_object_key, + level_background_image_src, + level_background_image_object_key, + ui_spritesheet_image_src, + ui_spritesheet_image_object_key, background_music: current_profile_level .as_ref() .and_then(|level| level.background_music.clone()), @@ -1373,10 +1484,29 @@ pub fn advance_next_level_at( current_profile_level.as_ref(), ui_background_level.as_ref(), ); + let level_background_level = first_profile_level_background_level(next_profile); + let (mut level_background_image_src, mut level_background_image_object_key) = + resolve_puzzle_runtime_level_background_fields( + current_profile_level.as_ref(), + level_background_level.as_ref(), + ); + let (mut ui_spritesheet_image_src, mut ui_spritesheet_image_object_key) = + resolve_puzzle_runtime_ui_spritesheet_fields( + current_profile_level.as_ref(), + next_profile.levels.first(), + ); if ui_background_image_src.is_none() && ui_background_image_object_key.is_none() { ui_background_image_src = current_level.ui_background_image_src.clone(); ui_background_image_object_key = current_level.ui_background_image_object_key.clone(); } + if level_background_image_src.is_none() && level_background_image_object_key.is_none() { + level_background_image_src = current_level.level_background_image_src.clone(); + level_background_image_object_key = current_level.level_background_image_object_key.clone(); + } + if ui_spritesheet_image_src.is_none() && ui_spritesheet_image_object_key.is_none() { + ui_spritesheet_image_src = current_level.ui_spritesheet_image_src.clone(); + ui_spritesheet_image_object_key = current_level.ui_spritesheet_image_object_key.clone(); + } Ok(PuzzleRunSnapshot { run_id: run.run_id.clone(), @@ -1400,6 +1530,10 @@ pub fn advance_next_level_at( cover_image_src: next_profile.cover_image_src.clone(), ui_background_image_src, ui_background_image_object_key, + level_background_image_src, + level_background_image_object_key, + ui_spritesheet_image_src, + ui_spritesheet_image_object_key, background_music: current_profile_level .as_ref() .and_then(|level| level.background_music.clone()), @@ -1461,6 +1595,17 @@ pub fn advance_to_new_work_first_level_at( current_profile_level.as_ref(), ui_background_level.as_ref(), ); + let level_background_level = first_profile_level_background_level(next_profile); + let (level_background_image_src, level_background_image_object_key) = + resolve_puzzle_runtime_level_background_fields( + current_profile_level.as_ref(), + level_background_level.as_ref(), + ); + let (ui_spritesheet_image_src, ui_spritesheet_image_object_key) = + resolve_puzzle_runtime_ui_spritesheet_fields( + current_profile_level.as_ref(), + next_profile.levels.first(), + ); Ok(PuzzleRunSnapshot { run_id: run.run_id.clone(), @@ -1484,6 +1629,10 @@ pub fn advance_to_new_work_first_level_at( cover_image_src: next_profile.cover_image_src.clone(), ui_background_image_src, ui_background_image_object_key, + level_background_image_src, + level_background_image_object_key, + ui_spritesheet_image_src, + ui_spritesheet_image_object_key, background_music: current_profile_level .as_ref() .and_then(|level| level.background_music.clone()), @@ -2900,6 +3049,12 @@ mod tests { ui_background_prompt: None, ui_background_image_src: None, ui_background_image_object_key: None, + level_scene_image_src: None, + level_scene_image_object_key: None, + ui_spritesheet_image_src: None, + ui_spritesheet_image_object_key: None, + level_background_image_src: None, + level_background_image_object_key: None, background_music: None, candidates: Vec::new(), selected_candidate_id: None, @@ -3118,6 +3273,12 @@ mod tests { ui_background_prompt: None, ui_background_image_src: None, ui_background_image_object_key: None, + level_scene_image_src: None, + level_scene_image_object_key: None, + ui_spritesheet_image_src: None, + ui_spritesheet_image_object_key: None, + level_background_image_src: None, + level_background_image_object_key: None, background_music: None, candidates: Vec::new(), selected_candidate_id: None, @@ -3133,6 +3294,12 @@ mod tests { ui_background_prompt: None, ui_background_image_src: None, ui_background_image_object_key: None, + level_scene_image_src: None, + level_scene_image_object_key: None, + ui_spritesheet_image_src: None, + ui_spritesheet_image_object_key: None, + level_background_image_src: None, + level_background_image_object_key: None, background_music: None, candidates: Vec::new(), selected_candidate_id: None, @@ -3248,6 +3415,12 @@ mod tests { ui_background_prompt: None, ui_background_image_src: None, ui_background_image_object_key: None, + level_scene_image_src: None, + level_scene_image_object_key: None, + ui_spritesheet_image_src: None, + ui_spritesheet_image_object_key: None, + level_background_image_src: None, + level_background_image_object_key: None, background_music: None, candidates: Vec::new(), selected_candidate_id: None, diff --git a/server-rs/crates/module-puzzle/src/creative_tools.rs b/server-rs/crates/module-puzzle/src/creative_tools.rs index 212d63be..a4199928 100644 --- a/server-rs/crates/module-puzzle/src/creative_tools.rs +++ b/server-rs/crates/module-puzzle/src/creative_tools.rs @@ -172,6 +172,12 @@ pub fn build_puzzle_draft_from_creative_fields( ui_background_prompt: None, ui_background_image_src: None, ui_background_image_object_key: None, + level_scene_image_src: None, + level_scene_image_object_key: None, + ui_spritesheet_image_src: None, + ui_spritesheet_image_object_key: None, + level_background_image_src: None, + level_background_image_object_key: None, background_music: None, candidates: Vec::new(), selected_candidate_id: None, diff --git a/server-rs/crates/module-puzzle/src/domain.rs b/server-rs/crates/module-puzzle/src/domain.rs index 7cd827e2..444f58e0 100644 --- a/server-rs/crates/module-puzzle/src/domain.rs +++ b/server-rs/crates/module-puzzle/src/domain.rs @@ -138,6 +138,18 @@ pub struct PuzzleDraftLevel { #[serde(default)] pub ui_background_image_object_key: Option, #[serde(default)] + pub level_scene_image_src: Option, + #[serde(default)] + pub level_scene_image_object_key: Option, + #[serde(default)] + pub ui_spritesheet_image_src: Option, + #[serde(default)] + pub ui_spritesheet_image_object_key: Option, + #[serde(default)] + pub level_background_image_src: Option, + #[serde(default)] + pub level_background_image_object_key: Option, + #[serde(default)] pub background_music: Option, pub candidates: Vec, pub selected_candidate_id: Option, @@ -367,6 +379,14 @@ pub struct PuzzleRuntimeLevelSnapshot { #[serde(default)] pub ui_background_image_object_key: Option, #[serde(default)] + pub level_background_image_src: Option, + #[serde(default)] + pub level_background_image_object_key: Option, + #[serde(default)] + pub ui_spritesheet_image_src: Option, + #[serde(default)] + pub ui_spritesheet_image_object_key: Option, + #[serde(default)] pub background_music: Option, pub board: PuzzleBoardSnapshot, pub status: PuzzleRuntimeLevelStatus, diff --git a/server-rs/crates/module-runtime-story/src/battle_tests.rs b/server-rs/crates/module-runtime-story/src/battle_tests.rs index a7d41413..51b9c3fe 100644 --- a/server-rs/crates/module-runtime-story/src/battle_tests.rs +++ b/server-rs/crates/module-runtime-story/src/battle_tests.rs @@ -5,8 +5,8 @@ use shared_contracts::runtime_story::{ }; use crate::{ - battle::resolve_battle_action, build_status_patch, read_bool_field, read_i32_field, - read_optional_string_field, + StoryRuntimeActionResolveInput, battle::resolve_battle_action, build_status_patch, + read_bool_field, read_i32_field, read_optional_string_field, resolve_story_runtime_action, }; fn build_battle_fixture() -> serde_json::Value { @@ -61,6 +61,115 @@ fn build_request(function_id: &str, option_text: &str) -> RuntimeStoryActionRequ } } +fn build_runtime_action_request( + function_id: &str, + action_text: &str, + payload: Option, +) -> shared_contracts::story::ResolveStoryRuntimeActionRequest { + shared_contracts::story::ResolveStoryRuntimeActionRequest { + story_session_id: "storysess-1".to_string(), + client_version: Some(1), + function_id: function_id.to_string(), + action_text: action_text.to_string(), + target_id: None, + payload, + } +} + +fn build_custom_world_profile_with_two_landmarks() -> serde_json::Value { + json!({ + "id": "profile-1", + "name": "雾桥旧约", + "summary": "雾桥边的旧约正在复苏。", + "camp": { + "id": "camp-1", + "name": "雾桥营地", + "description": "营火压着雾气。", + "connections": [ + { + "targetLandmarkId": "landmark-1", + "relativePosition": "forward", + "summary": "沿桥面继续前进" + }, + { + "targetLandmarkId": "landmark-2", + "relativePosition": "right", + "summary": "转入雾中支路" + } + ] + }, + "landmarks": [ + { + "id": "landmark-1", + "name": "断桥口", + "description": "桥口挂着旧灯。" + }, + { + "id": "landmark-2", + "name": "雾中渡", + "description": "渡口只有潮声。" + } + ], + "storyNpcs": [ + { + "id": "npc-bridge", + "name": "桥影", + "description": "桥下逼来的敌影", + "initialAffinity": -20 + }, + { + "id": "npc-ferryman", + "name": "摆渡人", + "description": "守着雾中渡的人", + "initialAffinity": 0 + } + ], + "sceneChapterBlueprints": [ + { + "id": "chapter-camp", + "sceneId": "camp-1", + "linkedLandmarkIds": ["camp-1"], + "acts": [ + { + "id": "act-camp-1", + "sceneId": "camp-1", + "oppositeNpcId": "npc-bridge" + }, + { + "id": "act-camp-2", + "sceneId": "camp-1", + "oppositeNpcId": "npc-ferryman" + } + ] + }, + { + "id": "chapter-landmark-1", + "sceneId": "landmark-1", + "linkedLandmarkIds": ["landmark-1"], + "acts": [ + { + "id": "act-landmark-1", + "sceneId": "landmark-1", + "oppositeNpcId": "npc-ferryman" + } + ] + } + ] + }) +} + +fn build_story_runtime_snapshot( + game_state: serde_json::Value, + current_story: Option, +) -> shared_contracts::story::StoryRuntimeSnapshotPayload { + shared_contracts::story::StoryRuntimeSnapshotPayload { + saved_at: None, + bottom_tab: "adventure".to_string(), + game_state, + current_story, + } +} + #[test] fn battle_resolution_prefers_player_defeat_when_both_sides_fall_in_same_turn() { let request = build_request("battle_all_in_crush", "全力压制"); @@ -89,3 +198,210 @@ fn battle_resolution_prefers_player_defeat_when_both_sides_fall_in_same_turn() { Some("defeat".to_string()) ); } + +#[test] +fn terminal_battle_action_persists_post_battle_continue_story() { + let mut game_state = build_battle_fixture(); + game_state["runtimeSessionId"] = json!("runtime-1"); + game_state["currentScene"] = json!("Story"); + game_state["worldType"] = json!("CUSTOM"); + game_state["playerHp"] = json!(30); + game_state["customWorldProfile"] = build_custom_world_profile_with_two_landmarks(); + game_state["currentScenePreset"] = json!({ + "id": "custom-scene-camp", + "name": "雾桥营地", + "description": "营火压着雾气。", + "connectedSceneIds": ["custom-scene-landmark-1", "custom-scene-landmark-2"], + "forwardSceneId": "custom-scene-landmark-1", + "treasureHints": [], + "npcs": [] + }); + game_state["storyEngineMemory"] = json!({ + "currentSceneActState": { + "sceneId": "camp-1", + "chapterId": "chapter-camp", + "currentActId": "act-camp-1", + "currentActIndex": 0, + "completedActIds": [], + "visitedActIds": ["act-camp-1"] + } + }); + + let output = resolve_story_runtime_action(StoryRuntimeActionResolveInput { + story_session_id: "storysess-1".to_string(), + runtime_session_id: "runtime-1".to_string(), + snapshot: build_story_runtime_snapshot(game_state, None), + request: build_runtime_action_request("battle_all_in_crush", "全力压制", None), + }) + .expect("terminal battle should resolve"); + + assert_eq!( + output.presentation.battle.unwrap().outcome.as_deref(), + Some("victory") + ); + assert_eq!( + output.presentation.options[0].function_id, + "story_continue_adventure" + ); + assert_eq!( + output.snapshot.current_story.as_ref().unwrap()["options"][0]["functionId"], + json!("story_continue_adventure") + ); + assert!( + output.snapshot.current_story.as_ref().unwrap()["deferredOptions"] + .as_array() + .is_some_and(|items| { + items + .iter() + .any(|item| item["functionId"] == json!("idle_travel_next_scene")) + }) + ); + assert_eq!( + output.snapshot.current_story.as_ref().unwrap()["deferredRuntimeState"]["storyEngineMemory"] + ["currentSceneActState"]["currentActId"], + json!("act-camp-2") + ); + assert_eq!( + output.snapshot.game_state["storyEngineMemory"]["currentSceneActState"]["currentActId"], + json!("act-camp-2") + ); +} + +#[test] +fn idle_travel_next_scene_changes_scene_from_target_payload() { + let game_state = json!({ + "runtimeSessionId": "runtime-1", + "runtimeActionVersion": 1, + "currentScene": "Story", + "worldType": "CUSTOM", + "customWorldProfile": build_custom_world_profile_with_two_landmarks(), + "playerHp": 30, + "playerMaxHp": 40, + "playerMana": 10, + "playerMaxMana": 20, + "playerCurrency": 0, + "playerInventory": [], + "playerEquipment": { "weapon": null, "armor": null, "relic": null }, + "runtimeStats": { + "hostileNpcsDefeated": 0, + "itemsUsed": 0, + "questsAccepted": 0, + "scenesTraveled": 0, + "playTimeMs": 0, + "lastPlayTickAt": null + }, + "currentScenePreset": { + "id": "custom-scene-camp", + "name": "雾桥营地", + "description": "营火压着雾气。", + "connectedSceneIds": ["custom-scene-landmark-1", "custom-scene-landmark-2"], + "connections": [ + { + "sceneId": "custom-scene-landmark-1", + "relativePosition": "forward", + "summary": "沿桥面继续前进" + } + ], + "forwardSceneId": "custom-scene-landmark-1", + "treasureHints": [], + "npcs": [] + }, + "currentEncounter": null, + "npcInteractionActive": false, + "sceneHostileNpcs": [], + "inBattle": false, + "storyHistory": [], + "storyEngineMemory": {} + }); + + let output = resolve_story_runtime_action(StoryRuntimeActionResolveInput { + story_session_id: "storysess-1".to_string(), + runtime_session_id: "runtime-1".to_string(), + snapshot: build_story_runtime_snapshot(game_state, None), + request: build_runtime_action_request( + "idle_travel_next_scene", + "向前走,前往断桥口", + Some(json!({ "targetSceneId": "custom-scene-landmark-1" })), + ), + }) + .expect("travel action should resolve"); + + assert_eq!( + output.snapshot.game_state["currentScenePreset"]["id"], + json!("custom-scene-landmark-1") + ); + assert_eq!( + output.snapshot.game_state["runtimeStats"]["scenesTraveled"], + json!(1) + ); + assert_eq!( + output.snapshot.game_state["currentEncounter"]["id"], + json!("npc-ferryman") + ); + assert_eq!( + output.snapshot.game_state["storyEngineMemory"]["currentSceneActState"]["currentActId"], + json!("act-landmark-1") + ); + assert!(output.presentation.options.iter().any(|option| { + option.function_id == "idle_travel_next_scene" + || option.function_id == "idle_explore_forward" + })); +} + +#[test] +fn idle_travel_next_scene_normalizes_custom_landmark_id_payload() { + let game_state = json!({ + "runtimeSessionId": "runtime-1", + "runtimeActionVersion": 1, + "currentScene": "Story", + "worldType": "CUSTOM", + "customWorldProfile": build_custom_world_profile_with_two_landmarks(), + "playerHp": 30, + "playerMaxHp": 40, + "playerMana": 10, + "playerMaxMana": 20, + "playerCurrency": 0, + "playerInventory": [], + "playerEquipment": { "weapon": null, "armor": null, "relic": null }, + "runtimeStats": { + "hostileNpcsDefeated": 0, + "itemsUsed": 0, + "questsAccepted": 0, + "scenesTraveled": 0, + "playTimeMs": 0, + "lastPlayTickAt": null + }, + "currentScenePreset": { + "id": "custom-scene-camp", + "name": "雾桥营地", + "description": "营火压着雾气。", + "connectedSceneIds": ["landmark-1", "landmark-2"], + "forwardSceneId": "landmark-2", + "treasureHints": [], + "npcs": [] + }, + "currentEncounter": null, + "npcInteractionActive": false, + "sceneHostileNpcs": [], + "inBattle": false, + "storyHistory": [], + "storyEngineMemory": {} + }); + + let output = resolve_story_runtime_action(StoryRuntimeActionResolveInput { + story_session_id: "storysess-1".to_string(), + runtime_session_id: "runtime-1".to_string(), + snapshot: build_story_runtime_snapshot(game_state, None), + request: build_runtime_action_request( + "idle_travel_next_scene", + "前往雾中渡", + Some(json!({ "targetSceneId": "landmark-2" })), + ), + }) + .expect("raw custom landmark id should resolve"); + + assert_eq!( + output.snapshot.game_state["currentScenePreset"]["id"], + json!("custom-scene-landmark-2") + ); +} diff --git a/server-rs/crates/module-runtime-story/src/lib.rs b/server-rs/crates/module-runtime-story/src/lib.rs index 53e06300..485377f1 100644 --- a/server-rs/crates/module-runtime-story/src/lib.rs +++ b/server-rs/crates/module-runtime-story/src/lib.rs @@ -76,7 +76,9 @@ pub use options::{ build_static_runtime_story_option, build_story_option_from_runtime_option, infer_option_scope, }; pub use post_battle::{ - finalize_post_battle_resolution, is_terminal_battle_outcome, resolve_post_battle_story_options, + clear_post_battle_state, ensure_scene_act_state, ensure_scene_encounter_preview, + finalize_post_battle_resolution, is_terminal_battle_outcome, resolve_forward_scene_id, + resolve_post_battle_story_options, resolve_runtime_scene_preset, }; pub use projection::{StoryRuntimeProjectionSource, build_story_runtime_projection}; pub use prompt_context::{RuntimeStoryPromptContextExtras, build_runtime_story_prompt_context}; diff --git a/server-rs/crates/module-runtime-story/src/post_battle.rs b/server-rs/crates/module-runtime-story/src/post_battle.rs index f804bfa4..efeabe8f 100644 --- a/server-rs/crates/module-runtime-story/src/post_battle.rs +++ b/server-rs/crates/module-runtime-story/src/post_battle.rs @@ -2,10 +2,11 @@ use serde_json::{Value, json}; use shared_contracts::runtime_story::RuntimeStoryOptionView; use crate::{ - CONTINUE_ADVENTURE_FUNCTION_ID, build_static_runtime_story_option, + CONTINUE_ADVENTURE_FUNCTION_ID, build_custom_scene_preset, build_static_runtime_story_option, build_story_option_from_runtime_option, ensure_json_object, read_array_field, read_bool_field, - read_field, read_i32_field, read_object_field, read_optional_string_field, write_bool_field, - write_i32_field, write_null_field, write_string_field, + read_field, read_i32_field, read_object_field, read_optional_string_field, + resolve_custom_runtime_scene_id, write_bool_field, write_i32_field, write_null_field, + write_string_field, }; const WUXIA_FIRST_SCENE_ID: &str = "wuxia-bamboo-road"; @@ -36,6 +37,8 @@ pub fn finalize_post_battle_resolution( return None; } + let original_scene_act_state = current_scene_act_state(game_state); + if outcome == "defeat" { return Some(finalize_defeat_revive(game_state, fallback_options)); } @@ -45,6 +48,7 @@ pub fn finalize_post_battle_resolution( game_state, result_text, fallback_options, + original_scene_act_state, )); } @@ -64,13 +68,14 @@ fn finalize_victory_or_spar( game_state: &mut Value, result_text: &str, fallback_options: Vec, + original_scene_act_state: Option, ) -> PostBattleFinalization { clear_post_battle_state(game_state); let is_last_act = is_current_scene_act_last(game_state); let next_act_state = if is_last_act { None } else { - resolve_next_scene_act_runtime_state(game_state) + resolve_next_scene_act_runtime_state(game_state, original_scene_act_state.as_ref()) }; if let Some(next_act_state) = next_act_state { write_current_scene_act_state(game_state, next_act_state); @@ -141,7 +146,7 @@ fn finalize_defeat_revive( { write_current_scene_act_state(game_state, first_act_state); } - ensure_first_scene_encounter_preview(game_state); + ensure_scene_encounter_preview(game_state); let story_text = if first_scene.name.is_empty() { "你在战斗中倒下,随后重新醒来。".to_string() @@ -160,7 +165,7 @@ fn finalize_defeat_revive( } } -fn clear_post_battle_state(game_state: &mut Value) { +pub fn clear_post_battle_state(game_state: &mut Value) { write_null_field(game_state, "currentEncounter"); write_bool_field(game_state, "npcInteractionActive", false); ensure_json_object(game_state).insert("sceneHostileNpcs".to_string(), Value::Array(Vec::new())); @@ -421,7 +426,7 @@ fn write_first_scene(game_state: &mut Value, scene: &RuntimeScene) { ); } -fn ensure_first_scene_encounter_preview(game_state: &mut Value) { +pub fn ensure_scene_encounter_preview(game_state: &mut Value) { if read_bool_field(game_state, "inBattle").unwrap_or(false) { return; } @@ -436,7 +441,13 @@ fn ensure_first_scene_encounter_preview(game_state: &mut Value) { }; let scene_id = read_object_field(game_state, "currentScenePreset") .and_then(|scene| read_optional_string_field(scene, "id")); - let focus_npc_id = resolve_active_scene_act_focus_npc_id(profile, scene_id.as_deref()); + let current_act_id = current_scene_act_state(game_state) + .and_then(|state| read_optional_string_field(&state, "currentActId")); + let focus_npc_id = resolve_active_scene_act_focus_npc_id( + profile, + scene_id.as_deref(), + current_act_id.as_deref(), + ); let Some(focus_npc_id) = focus_npc_id else { return; }; @@ -450,6 +461,22 @@ fn ensure_first_scene_encounter_preview(game_state: &mut Value) { ); } +pub fn ensure_scene_act_state(game_state: &mut Value) { + if read_bool_field(game_state, "inBattle").unwrap_or(false) { + return; + } + let Some(scene_id) = read_object_field(game_state, "currentScenePreset") + .and_then(|scene| read_optional_string_field(scene, "id")) + else { + return; + }; + let Some(act_state) = build_initial_scene_act_runtime_state(game_state, scene_id.as_str()) + else { + return; + }; + write_current_scene_act_state(game_state, act_state); +} + fn build_scene_travel_options(game_state: &Value) -> Vec { let Some(current_scene) = read_object_field(game_state, "currentScenePreset") else { return vec![build_static_runtime_story_option( @@ -459,32 +486,53 @@ fn build_scene_travel_options(game_state: &Value) -> Vec )]; }; let current_scene_id = read_optional_string_field(current_scene, "id"); - let mut options = read_array_field(current_scene, "connections") + let forward_scene_id = read_optional_string_field(current_scene, "forwardSceneId"); + let mut option_scene_ids = Vec::new(); + let mut options = Vec::new(); + + for connection in read_array_field(current_scene, "connections") { + let Some(scene_id) = read_optional_string_field(connection, "sceneId") else { + continue; + }; + if current_scene_id.as_deref() == Some(scene_id.as_str()) + || option_scene_ids.iter().any(|id| id == scene_id.as_str()) + { + continue; + } + let relative_position = read_optional_string_field(connection, "relativePosition") + .unwrap_or_else(|| "forward".to_string()); + options.push(build_scene_travel_option( + game_state, + scene_id.as_str(), + relative_position.as_str(), + )); + option_scene_ids.push(scene_id); + } + + for scene_id in read_array_field(current_scene, "connectedSceneIds") .into_iter() - .filter_map(|connection| { - let scene_id = read_optional_string_field(connection, "sceneId")?; - if current_scene_id.as_deref() == Some(scene_id.as_str()) { - return None; - } - let relative_position = read_optional_string_field(connection, "relativePosition") - .unwrap_or_else(|| "forward".to_string()); - let scene_name = resolve_scene_name(game_state, scene_id.as_str()) - .unwrap_or_else(|| scene_id.clone()); - Some(RuntimeStoryOptionView { - payload: Some(json!({ "targetSceneId": scene_id })), - ..build_static_runtime_story_option( - "idle_travel_next_scene", - format!( - "{},前往{}", - direction_text(relative_position.as_str()), - scene_name - ) - .as_str(), - "story", - ) - }) - }) - .collect::>(); + .filter_map(|scene_id| scene_id.as_str().map(str::to_string)) + .chain(forward_scene_id.clone()) + { + // 中文注释:bootstrap 生成的旧快照常只有 connectedSceneIds / forwardSceneId, + // 没有展开 connections;这里也要生成旅行 action,避免战后只剩默认 idle 选项循环。 + if current_scene_id.as_deref() == Some(scene_id.as_str()) + || option_scene_ids.iter().any(|id| id == scene_id.as_str()) + { + continue; + } + let relative_position = if forward_scene_id.as_deref() == Some(scene_id.as_str()) { + "forward" + } else { + "portal" + }; + options.push(build_scene_travel_option( + game_state, + scene_id.as_str(), + relative_position, + )); + option_scene_ids.push(scene_id); + } if options.is_empty() { options.push(build_static_runtime_story_option( @@ -497,6 +545,163 @@ fn build_scene_travel_options(game_state: &Value) -> Vec options } +fn build_scene_travel_option( + game_state: &Value, + scene_id: &str, + relative_position: &str, +) -> RuntimeStoryOptionView { + let scene_name = + resolve_scene_name(game_state, scene_id).unwrap_or_else(|| scene_id.to_string()); + RuntimeStoryOptionView { + payload: Some(json!({ "targetSceneId": scene_id })), + ..build_static_runtime_story_option( + "idle_travel_next_scene", + format!("{},前往{}", direction_text(relative_position), scene_name).as_str(), + "story", + ) + } +} + +pub fn resolve_runtime_scene_preset(game_state: &Value, scene_id: &str) -> Option { + let normalized_scene_id = scene_id.trim(); + if normalized_scene_id.is_empty() { + return None; + } + + if let Some(profile) = read_object_field(game_state, "customWorldProfile") + && let Some(scene) = build_custom_scene_preset( + profile, + resolve_custom_runtime_scene_id(profile, normalized_scene_id).as_str(), + ) + { + return Some(scene); + } + + resolve_builtin_runtime_scene_preset(game_state, normalized_scene_id) +} + +pub fn resolve_forward_scene_id(game_state: &Value) -> Option { + read_object_field(game_state, "currentScenePreset").and_then(|scene| { + read_optional_string_field(scene, "forwardSceneId") + .or_else(|| { + read_array_field(scene, "connections") + .into_iter() + .find_map(|connection| read_optional_string_field(connection, "sceneId")) + }) + .or_else(|| { + read_array_field(scene, "connectedSceneIds") + .into_iter() + .find_map(|scene_id| scene_id.as_str().map(str::to_string)) + }) + }) +} + +fn resolve_builtin_runtime_scene_preset(game_state: &Value, scene_id: &str) -> Option { + let template = builtin_runtime_scene_template(scene_id)?; + Some(json!({ + "id": template.id, + "name": template.name, + "description": template.description, + "imageSrc": read_object_field(game_state, "currentScenePreset") + .and_then(|scene| read_optional_string_field(scene, "imageSrc")) + .unwrap_or_default(), + "connectedSceneIds": template.connected_scene_ids, + "connections": template.connections, + "forwardSceneId": template.forward_scene_id, + "treasureHints": template.treasure_hints, + "npcs": [], + })) +} + +fn builtin_runtime_scene_template(scene_id: &str) -> Option { + let is_xianxia = matches!( + scene_id, + "xianxia-cloud-gate" + | "xianxia-floating-isle" + | "xianxia-celestial-corridor" + | "xianxia-star-vessel" + ); + if is_xianxia { + return Some(RuntimeScene { + id: scene_id.to_string(), + name: match scene_id { + "xianxia-floating-isle" => "浮空灵岛", + "xianxia-celestial-corridor" => "天门长廊", + "xianxia-star-vessel" => "星槎泊台", + _ => XIANXIA_FIRST_SCENE_NAME, + } + .to_string(), + description: match scene_id { + "xianxia-floating-isle" => "浮岛边缘灵雾翻涌,远处有阵纹一明一暗。", + "xianxia-celestial-corridor" => "长廊悬在云海上方,符光沿石柱缓慢游走。", + "xianxia-star-vessel" => "星槎泊在云海边缘,船身仍有星砂微光。", + _ => XIANXIA_FIRST_SCENE_DESCRIPTION, + } + .to_string(), + image_src: String::new(), + connected_scene_ids: vec![ + "xianxia-cloud-gate".to_string(), + "xianxia-floating-isle".to_string(), + "xianxia-celestial-corridor".to_string(), + ] + .into_iter() + .filter(|id| id != scene_id) + .collect(), + connections: vec![json!({ + "sceneId": if scene_id == "xianxia-cloud-gate" { "xianxia-celestial-corridor" } else { "xianxia-cloud-gate" }, + "relativePosition": if scene_id == "xianxia-cloud-gate" { "forward" } else { "back" }, + "summary": "沿主路继续移动" + })], + forward_scene_id: Some(if scene_id == "xianxia-cloud-gate" { + "xianxia-celestial-corridor".to_string() + } else { + "xianxia-cloud-gate".to_string() + }), + treasure_hints: vec!["云阶边缘的灵光残痕".to_string()], + npcs: Vec::new(), + }); + } + + Some(RuntimeScene { + id: scene_id.to_string(), + name: match scene_id { + "wuxia-mountain-gate" => "山门石阶", + "wuxia-mist-woods" => "迷雾竹林", + "wuxia-ferry-bridge" => "渡口断桥", + _ => WUXIA_FIRST_SCENE_NAME, + } + .to_string(), + description: match scene_id { + "wuxia-mountain-gate" => "山门石阶覆着苔痕,旧旗在风里压得很低。", + "wuxia-mist-woods" => "迷雾在竹林间翻卷,脚下泥印很快又被雾水抹平。", + "wuxia-ferry-bridge" => "渡口断桥横在冷水上,桥边灯笼只剩半截残光。", + _ => WUXIA_FIRST_SCENE_DESCRIPTION, + } + .to_string(), + image_src: String::new(), + connected_scene_ids: vec![ + "wuxia-bamboo-road".to_string(), + "wuxia-mountain-gate".to_string(), + "wuxia-mist-woods".to_string(), + ] + .into_iter() + .filter(|id| id != scene_id) + .collect(), + connections: vec![json!({ + "sceneId": if scene_id == "wuxia-bamboo-road" { "wuxia-mountain-gate" } else { "wuxia-bamboo-road" }, + "relativePosition": if scene_id == "wuxia-bamboo-road" { "forward" } else { "back" }, + "summary": "沿主路继续移动" + })], + forward_scene_id: Some(if scene_id == "wuxia-bamboo-road" { + "wuxia-mountain-gate".to_string() + } else { + "wuxia-bamboo-road".to_string() + }), + treasure_hints: vec!["路边半埋的旧物".to_string()], + npcs: Vec::new(), + }) +} + fn resolve_scene_name(game_state: &Value, scene_id: &str) -> Option { if read_object_field(game_state, "currentScenePreset") .and_then(|scene| read_optional_string_field(scene, "id")) @@ -553,7 +758,10 @@ fn direction_text(relative_position: &str) -> &'static str { } } -fn resolve_next_scene_act_runtime_state(game_state: &Value) -> Option { +fn resolve_next_scene_act_runtime_state( + game_state: &Value, + current_act_state_override: Option<&Value>, +) -> Option { let profile = read_object_field(game_state, "customWorldProfile")?; let scene_id = read_object_field(game_state, "currentScenePreset") .and_then(|scene| read_optional_string_field(scene, "id")); @@ -563,7 +771,9 @@ fn resolve_next_scene_act_runtime_state(game_state: &Value) -> Option { if acts.is_empty() { return None; } - let runtime_state = build_initial_scene_act_runtime_state(game_state, scene_id_text)?; + let runtime_state = current_act_state_override + .cloned() + .or_else(|| build_initial_scene_act_runtime_state(game_state, scene_id_text))?; let current_act_id = read_optional_string_field(&runtime_state, "currentActId"); let current_index = acts .iter() @@ -762,9 +972,17 @@ fn resolve_scene_aliases(profile: &Value, scene_id: &str) -> Vec { fn resolve_active_scene_act_focus_npc_id( profile: &Value, scene_id: Option<&str>, + current_act_id: Option<&str>, ) -> Option { let chapter = resolve_scene_chapter_blueprint(profile, scene_id)?; - let act_state = read_array_field(chapter, "acts").first().copied()?; + let acts = read_array_field(chapter, "acts"); + let act_state = current_act_id + .and_then(|act_id| { + acts.iter() + .copied() + .find(|act| read_optional_string_field(act, "id").as_deref() == Some(act_id)) + }) + .or_else(|| acts.first().copied())?; read_optional_string_field(act_state, "oppositeNpcId") .or_else(|| read_optional_string_field(act_state, "primaryNpcId")) .or_else(|| { diff --git a/server-rs/crates/module-runtime-story/src/session_action.rs b/server-rs/crates/module-runtime-story/src/session_action.rs index c7ad6421..93d9b25a 100644 --- a/server-rs/crates/module-runtime-story/src/session_action.rs +++ b/server-rs/crates/module-runtime-story/src/session_action.rs @@ -14,16 +14,18 @@ use crate::{ build_current_build_toast, build_npc_gift_result_text, build_runtime_story_option_from_story_option, build_runtime_story_view_model, build_static_runtime_story_option, build_status_patch, build_story_option_from_runtime_option, - clear_encounter_state, clone_inventory_item_with_quantity, current_encounter_name, - ensure_json_object, find_player_inventory_entry, normalize_equipment_slot_id, - normalize_required_string, npc_buyback_price, npc_purchase_price, + clear_encounter_state, clear_post_battle_state, clone_inventory_item_with_quantity, + current_encounter_name, ensure_json_object, ensure_scene_act_state, + ensure_scene_encounter_preview, finalize_post_battle_resolution, find_player_inventory_entry, + normalize_equipment_slot_id, normalize_required_string, npc_buyback_price, npc_purchase_price, project_story_engine_after_action, read_array_field, read_bool_field, read_field, read_i32_field, read_inventory_item_name, read_object_field, read_optional_string_field, read_player_equipment_item, read_player_inventory_values, read_runtime_session_id, read_u32_field, recruit_companion_to_party, remove_inventory_item_from_list, resolve_action_text, resolve_battle_action, resolve_equipment_slot_for_item, resolve_forge_craft_action, resolve_forge_dismantle_action, resolve_forge_reforge_action, - resolve_npc_gift_affinity_gain, restore_player_resource, simple_story_resolution, + resolve_forward_scene_id, resolve_npc_gift_affinity_gain, resolve_post_battle_story_options, + resolve_runtime_scene_preset, restore_player_resource, simple_story_resolution, write_bool_field, write_i32_field, write_null_field, write_player_equipment_item, write_player_inventory_values, write_runtime_npc_interaction_view, write_string_field, write_u32_field, @@ -97,23 +99,7 @@ pub fn resolve_story_runtime_action( requested_runtime_session_id.as_str(), ); - let mut options = resolution - .presentation_options - .take() - .unwrap_or_else(|| build_fallback_runtime_story_options(&game_state)); - if options.is_empty() { - options = build_fallback_runtime_story_options(&game_state); - } - - let story_text = resolution - .story_text - .clone() - .unwrap_or_else(|| resolution.result_text.clone()); let history_result_text = resolution.result_text.clone(); - let saved_current_story = resolution - .saved_current_story - .take() - .unwrap_or_else(|| build_current_story(story_text.as_str(), &options)); append_story_history( &mut game_state, @@ -132,6 +118,37 @@ pub fn resolve_story_runtime_action( .and_then(|battle| battle.outcome.as_deref()), ); + if let Some(post_battle) = finalize_post_battle_resolution( + &mut game_state, + history_result_text.as_str(), + resolution + .battle + .as_ref() + .and_then(|battle| battle.outcome.as_deref()), + Vec::new(), + ) { + resolution.story_text = Some(post_battle.story_text); + resolution.presentation_options = Some(post_battle.presentation_options); + resolution.saved_current_story = Some(post_battle.saved_current_story); + } + + let mut options = resolution + .presentation_options + .take() + .unwrap_or_else(|| build_fallback_runtime_story_options(&game_state)); + if options.is_empty() { + options = build_fallback_runtime_story_options(&game_state); + } + + let story_text = resolution + .story_text + .clone() + .unwrap_or_else(|| resolution.result_text.clone()); + let saved_current_story = resolution + .saved_current_story + .take() + .unwrap_or_else(|| build_current_story(story_text.as_str(), &options)); + let mut patches = vec![RuntimeStoryPatch::StoryHistoryAppend { action_text: resolution.action_text.clone(), result_text: history_result_text.clone(), @@ -212,11 +229,10 @@ fn resolve_runtime_story_choice_action( resolve_action_text("主动出声试探", request), "你的喊话打破了当前静场,周围潜着的动静也更难继续藏住。", )), - "idle_explore_forward" => Ok(simple_story_resolution( - game_state, - resolve_action_text("继续向前探索", request), - "你没有停在原地,而是继续向前压,把下一段遭遇主动推到自己面前。", - )), + "idle_explore_forward" => resolve_idle_explore_forward_action(game_state, request), + "idle_travel_next_scene" | "camp_travel_home_scene" => { + resolve_idle_travel_next_scene_action(game_state, request) + } "idle_observe_signs" => Ok(simple_story_resolution( game_state, resolve_action_text("观察周围迹象", request), @@ -309,6 +325,62 @@ fn resolve_continue_adventure_action( }) } +fn resolve_idle_explore_forward_action( + game_state: &mut Value, + request: &RuntimeStoryActionRequest, +) -> Result { + // 中文注释:探索前进是战后继续链路的一环,必须在后端清掉战斗态并生成下一段遭遇预览。 + // 前端只播放表现动画,不能只靠本地状态把同一组 idle 选项重新展示一遍。 + clear_post_battle_state(game_state); + ensure_scene_encounter_preview(game_state); + Ok(StoryResolution { + action_text: resolve_action_text("继续向前探索", request), + result_text: "你没有停在原地,而是继续向前压,把下一段遭遇主动推到自己面前。".to_string(), + story_text: None, + presentation_options: Some(resolve_post_battle_story_options(game_state)), + saved_current_story: None, + patches: vec![build_status_patch(game_state)], + battle: None, + toast: None, + }) +} + +fn resolve_idle_travel_next_scene_action( + game_state: &mut Value, + request: &RuntimeStoryActionRequest, +) -> Result { + // 中文注释:切场景会改变 currentScenePreset、章节 act 状态和运行统计, + // 这些都是 runtime 快照真相,不能只在前端播放退场/进场动画。 + let payload = request.action.payload.as_ref(); + let target_scene_id = payload + .and_then(|payload| read_optional_string_field(payload, "targetSceneId")) + .or_else(|| resolve_forward_scene_id(game_state)) + .ok_or_else(|| "idle_travel_next_scene 缺少 targetSceneId".to_string())?; + let next_scene = resolve_runtime_scene_preset(game_state, target_scene_id.as_str()) + .ok_or_else(|| format!("未找到目标场景:{target_scene_id}"))?; + let next_scene_name = + read_optional_string_field(&next_scene, "name").unwrap_or_else(|| target_scene_id.clone()); + + clear_post_battle_state(game_state); + ensure_json_object(game_state).insert("currentScenePreset".to_string(), next_scene); + write_i32_field(game_state, "playerX", 0); + write_string_field(game_state, "playerFacing", "right"); + ensure_scene_act_state(game_state); + ensure_scene_encounter_preview(game_state); + increment_runtime_stat_local(game_state, "scenesTraveled", 1); + + Ok(StoryResolution { + action_text: resolve_action_text(&format!("前往{next_scene_name}"), request), + result_text: format!("你离开当前区域,抵达了{next_scene_name}。"), + story_text: None, + presentation_options: Some(resolve_post_battle_story_options(game_state)), + saved_current_story: None, + patches: vec![build_status_patch(game_state)], + battle: None, + toast: None, + }) +} + fn resolve_npc_preview_talk_action( game_state: &mut Value, request: &RuntimeStoryActionRequest, diff --git a/server-rs/crates/module-runtime/src/application.rs b/server-rs/crates/module-runtime/src/application.rs index 8a578c5e..5c86bbd6 100644 --- a/server-rs/crates/module-runtime/src/application.rs +++ b/server-rs/crates/module-runtime/src/application.rs @@ -54,9 +54,9 @@ pub fn default_creation_entry_type_snapshots( "rpg", "文字冒险", "经典 RPG 体验", - "内测", + "可创建", "/creation-type-references/rpg.webp", - false, + true, true, 10, updated_at_micros, @@ -94,6 +94,17 @@ pub fn default_creation_entry_type_snapshots( 40, updated_at_micros, ), + build_default_creation_entry_type_snapshot( + "jump-hop", + "跳一跳", + "俯视角跳跃闯关", + "可创建", + "/creation-type-references/puzzle.webp", + true, + true, + 45, + updated_at_micros, + ), build_default_creation_entry_type_snapshot( "square-hole", "方洞", diff --git a/server-rs/crates/module-runtime/src/lib.rs b/server-rs/crates/module-runtime/src/lib.rs index 6f5d75e4..9c2fd42f 100644 --- a/server-rs/crates/module-runtime/src/lib.rs +++ b/server-rs/crates/module-runtime/src/lib.rs @@ -236,6 +236,23 @@ mod tests { ); } + #[test] + fn default_creation_entry_types_open_rpg_entry() { + let configs = default_creation_entry_type_snapshots(1); + let rpg = configs + .iter() + .find(|item| item.id == "rpg") + .expect("rpg creation entry should be seeded"); + + assert_eq!(rpg.title, "文字冒险"); + assert_eq!(rpg.subtitle, "经典 RPG 体验"); + assert!(rpg.visible); + assert!(rpg.open); + assert_eq!(rpg.badge, "可创建"); + assert_eq!(rpg.sort_order, 10); + assert_eq!(rpg.image_src, "/creation-type-references/rpg.webp"); + } + #[test] fn default_creation_entry_types_include_bark_battle() { let configs = default_creation_entry_type_snapshots(1); diff --git a/server-rs/crates/shared-contracts/src/jump_hop.rs b/server-rs/crates/shared-contracts/src/jump_hop.rs new file mode 100644 index 00000000..e4d4657d --- /dev/null +++ b/server-rs/crates/shared-contracts/src/jump_hop.rs @@ -0,0 +1,401 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum JumpHopDifficulty { + Easy, + Standard, + Advanced, + Challenge, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum JumpHopStylePreset { + MinimalBlocks, + PaperToy, + NeonGlass, + ForestStone, + FutureMetal, + Custom, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum JumpHopGenerationStatus { + Draft, + Generating, + Ready, + Failed, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum JumpHopTileType { + Start, + Normal, + Target, + Finish, + Bonus, + Accent, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum JumpHopActionType { + CompileDraft, + RegenerateCharacter, + RegenerateTiles, + UpdateWorkMeta, + UpdateDifficulty, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum JumpHopRunStatus { + Playing, + Failed, + Cleared, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum JumpHopJumpResult { + Miss, + Hit, + Perfect, + Finish, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopWorkspaceCreateRequest { + pub template_id: String, + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + pub difficulty: JumpHopDifficulty, + pub style_preset: JumpHopStylePreset, + pub character_prompt: String, + pub tile_prompt: String, + #[serde(default)] + pub end_mood_prompt: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopActionRequest { + pub action_type: JumpHopActionType, + #[serde(default)] + pub work_title: Option, + #[serde(default)] + pub work_description: Option, + #[serde(default)] + pub theme_tags: Option>, + #[serde(default)] + pub difficulty: Option, + #[serde(default)] + pub style_preset: Option, + #[serde(default)] + pub character_prompt: Option, + #[serde(default)] + pub tile_prompt: Option, + #[serde(default)] + pub end_mood_prompt: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopCharacterAsset { + pub asset_id: String, + pub image_src: String, + pub image_object_key: String, + pub asset_object_id: String, + pub generation_provider: String, + pub prompt: String, + pub width: u32, + pub height: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopTileAsset { + pub tile_type: JumpHopTileType, + pub image_src: String, + pub image_object_key: String, + pub asset_object_id: String, + pub source_atlas_cell: String, + pub visual_width: u32, + pub visual_height: u32, + pub top_surface_radius: f32, + pub landing_radius: f32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopScoring { + pub charge_to_distance_ratio: f32, + pub max_charge_ms: u32, + pub hit_bonus: u32, + pub perfect_bonus: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopPlatform { + pub platform_id: String, + pub tile_type: JumpHopTileType, + pub x: f32, + pub y: f32, + pub width: f32, + pub height: f32, + pub landing_radius: f32, + pub perfect_radius: f32, + pub score_value: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopPath { + pub seed: String, + pub difficulty: JumpHopDifficulty, + pub platforms: Vec, + pub finish_index: u32, + pub camera_preset: String, + pub scoring: JumpHopScoring, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopLastJump { + pub charge_ms: u32, + pub jump_distance: f32, + pub target_platform_index: u32, + pub landed_x: f32, + pub landed_y: f32, + pub result: JumpHopJumpResult, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopDraftResponse { + pub template_id: String, + pub template_name: String, + #[serde(default)] + pub profile_id: Option, + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + pub difficulty: JumpHopDifficulty, + pub style_preset: JumpHopStylePreset, + pub character_prompt: String, + pub tile_prompt: String, + #[serde(default)] + pub end_mood_prompt: Option, + #[serde(default)] + pub character_asset: Option, + #[serde(default)] + pub tile_atlas_asset: Option, + #[serde(default)] + pub tile_assets: Vec, + #[serde(default)] + pub path: Option, + #[serde(default)] + pub cover_composite: Option, + pub generation_status: JumpHopGenerationStatus, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopSessionSnapshotResponse { + pub session_id: String, + pub owner_user_id: String, + pub status: JumpHopGenerationStatus, + #[serde(default)] + pub draft: Option, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopSessionResponse { + pub session: JumpHopSessionSnapshotResponse, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopActionResponse { + pub action_type: JumpHopActionType, + pub session: JumpHopSessionSnapshotResponse, + #[serde(default)] + pub work: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopWorkSummaryResponse { + pub runtime_kind: String, + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + #[serde(default)] + pub source_session_id: Option, + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + pub difficulty: JumpHopDifficulty, + pub style_preset: JumpHopStylePreset, + #[serde(default)] + pub cover_image_src: Option, + pub publication_status: String, + pub play_count: u32, + pub updated_at: String, + #[serde(default)] + pub published_at: Option, + pub publish_ready: bool, + pub generation_status: JumpHopGenerationStatus, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopWorkProfileResponse { + #[serde(flatten)] + pub summary: JumpHopWorkSummaryResponse, + pub draft: JumpHopDraftResponse, + pub path: JumpHopPath, + pub character_asset: JumpHopCharacterAsset, + pub tile_atlas_asset: JumpHopCharacterAsset, + pub tile_assets: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopWorksResponse { + pub items: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopWorkDetailResponse { + pub item: JumpHopWorkProfileResponse, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopWorkMutationResponse { + pub item: JumpHopWorkProfileResponse, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopGalleryCardResponse { + pub public_work_code: String, + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + #[serde(default)] + pub cover_image_src: Option, + pub theme_tags: Vec, + pub difficulty: JumpHopDifficulty, + pub style_preset: JumpHopStylePreset, + pub publication_status: String, + pub play_count: u32, + pub updated_at: String, + #[serde(default)] + pub published_at: Option, + pub generation_status: JumpHopGenerationStatus, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopGalleryResponse { + pub items: Vec, + pub has_more: bool, + #[serde(default)] + pub next_cursor: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopGalleryDetailResponse { + pub item: JumpHopWorkProfileResponse, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopRuntimeRunSnapshotResponse { + pub run_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub status: JumpHopRunStatus, + pub current_platform_index: u32, + pub score: u32, + pub combo: u32, + pub path: JumpHopPath, + #[serde(default)] + pub last_jump: Option, + pub started_at_ms: u64, + #[serde(default)] + pub finished_at_ms: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopRunResponse { + pub run: JumpHopRuntimeRunSnapshotResponse, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopStartRunRequest { + pub profile_id: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopJumpRequest { + pub charge_ms: u32, + pub client_event_id: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopRestartRunRequest { + pub client_action_id: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopJumpResponse { + pub run: JumpHopRuntimeRunSnapshotResponse, +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn jump_hop_workspace_request_uses_camel_case() { + let payload = serde_json::to_value(JumpHopWorkspaceCreateRequest { + template_id: "jump-hop".to_string(), + work_title: "跳一跳".to_string(), + work_description: "俯视角跳跃闯关".to_string(), + theme_tags: vec!["休闲".to_string()], + difficulty: JumpHopDifficulty::Easy, + style_preset: JumpHopStylePreset::MinimalBlocks, + character_prompt: "角色".to_string(), + tile_prompt: "地块".to_string(), + end_mood_prompt: None, + }) + .expect("payload should serialize"); + + assert_eq!(payload["templateId"], json!("jump-hop")); + assert_eq!(payload["difficulty"], json!("easy")); + assert_eq!(payload["stylePreset"], json!("minimal-blocks")); + } +} diff --git a/server-rs/crates/shared-contracts/src/lib.rs b/server-rs/crates/shared-contracts/src/lib.rs index 19c713fd..5324480d 100644 --- a/server-rs/crates/shared-contracts/src/lib.rs +++ b/server-rs/crates/shared-contracts/src/lib.rs @@ -12,6 +12,7 @@ pub mod creation_audio; pub mod creation_entry_config; pub mod creative_agent; pub mod hyper3d; +pub mod jump_hop; pub mod llm; pub mod match3d_agent; pub mod match3d_runtime; diff --git a/server-rs/crates/shared-contracts/src/match3d_agent.rs b/server-rs/crates/shared-contracts/src/match3d_agent.rs index b64d140a..46365eec 100644 --- a/server-rs/crates/shared-contracts/src/match3d_agent.rs +++ b/server-rs/crates/shared-contracts/src/match3d_agent.rs @@ -110,10 +110,28 @@ pub struct Match3DResultDraftResponse { pub struct Match3DGeneratedBackgroundAssetResponse { pub prompt: String, #[serde(default)] + pub level_scene_prompt: Option, + #[serde(default)] + pub level_scene_image_src: Option, + #[serde(default)] + pub level_scene_image_object_key: Option, + #[serde(default)] pub image_src: Option, #[serde(default)] pub image_object_key: Option, #[serde(default)] + pub ui_spritesheet_prompt: Option, + #[serde(default)] + pub ui_spritesheet_image_src: Option, + #[serde(default)] + pub ui_spritesheet_image_object_key: Option, + #[serde(default)] + pub item_spritesheet_prompt: Option, + #[serde(default)] + pub item_spritesheet_image_src: Option, + #[serde(default)] + pub item_spritesheet_image_object_key: Option, + #[serde(default)] pub container_prompt: Option, #[serde(default)] pub container_image_src: Option, diff --git a/server-rs/crates/shared-contracts/src/match3d_works.rs b/server-rs/crates/shared-contracts/src/match3d_works.rs index bc68558b..1536d45f 100644 --- a/server-rs/crates/shared-contracts/src/match3d_works.rs +++ b/server-rs/crates/shared-contracts/src/match3d_works.rs @@ -170,10 +170,28 @@ pub struct Match3DWorkSummaryResponse { pub struct Match3DGeneratedBackgroundAssetResponse { pub prompt: String, #[serde(default)] + pub level_scene_prompt: Option, + #[serde(default)] + pub level_scene_image_src: Option, + #[serde(default)] + pub level_scene_image_object_key: Option, + #[serde(default)] pub image_src: Option, #[serde(default)] pub image_object_key: Option, #[serde(default)] + pub ui_spritesheet_prompt: Option, + #[serde(default)] + pub ui_spritesheet_image_src: Option, + #[serde(default)] + pub ui_spritesheet_image_object_key: Option, + #[serde(default)] + pub item_spritesheet_prompt: Option, + #[serde(default)] + pub item_spritesheet_image_src: Option, + #[serde(default)] + pub item_spritesheet_image_object_key: Option, + #[serde(default)] pub container_prompt: Option, #[serde(default)] pub container_image_src: Option, diff --git a/server-rs/crates/shared-contracts/src/puzzle_agent.rs b/server-rs/crates/shared-contracts/src/puzzle_agent.rs index aec6e3e9..b417ca80 100644 --- a/server-rs/crates/shared-contracts/src/puzzle_agent.rs +++ b/server-rs/crates/shared-contracts/src/puzzle_agent.rs @@ -18,6 +18,10 @@ pub struct CreatePuzzleAgentSessionRequest { #[serde(default)] pub reference_image_srcs: Vec, #[serde(default)] + pub reference_image_asset_object_id: Option, + #[serde(default)] + pub reference_image_asset_object_ids: Vec, + #[serde(default)] pub image_model: Option, #[serde(default)] pub ai_redraw: Option, @@ -43,6 +47,10 @@ pub struct ExecutePuzzleAgentActionRequest { #[serde(default)] pub reference_image_srcs: Vec, #[serde(default)] + pub reference_image_asset_object_id: Option, + #[serde(default)] + pub reference_image_asset_object_ids: Vec, + #[serde(default)] pub image_model: Option, #[serde(default)] pub ai_redraw: Option, @@ -166,6 +174,18 @@ pub struct PuzzleDraftLevelResponse { #[serde(default)] pub ui_background_image_object_key: Option, #[serde(default)] + pub level_scene_image_src: Option, + #[serde(default)] + pub level_scene_image_object_key: Option, + #[serde(default)] + pub ui_spritesheet_image_src: Option, + #[serde(default)] + pub ui_spritesheet_image_object_key: Option, + #[serde(default)] + pub level_background_image_src: Option, + #[serde(default)] + pub level_background_image_object_key: Option, + #[serde(default)] pub background_music: Option, pub candidates: Vec, #[serde(default)] diff --git a/server-rs/crates/shared-contracts/src/puzzle_runtime.rs b/server-rs/crates/shared-contracts/src/puzzle_runtime.rs index c5101734..c85e3e57 100644 --- a/server-rs/crates/shared-contracts/src/puzzle_runtime.rs +++ b/server-rs/crates/shared-contracts/src/puzzle_runtime.rs @@ -122,6 +122,14 @@ pub struct PuzzleRuntimeLevelSnapshotResponse { #[serde(default)] pub ui_background_image_object_key: Option, #[serde(default)] + pub level_background_image_src: Option, + #[serde(default)] + pub level_background_image_object_key: Option, + #[serde(default)] + pub ui_spritesheet_image_src: Option, + #[serde(default)] + pub ui_spritesheet_image_object_key: Option, + #[serde(default)] pub background_music: Option, pub board: PuzzleBoardSnapshotResponse, pub status: String, diff --git a/server-rs/crates/shared-logging/src/lib.rs b/server-rs/crates/shared-logging/src/lib.rs index ad77a6fb..6ab9470d 100644 --- a/server-rs/crates/shared-logging/src/lib.rs +++ b/server-rs/crates/shared-logging/src/lib.rs @@ -4,10 +4,7 @@ use opentelemetry::{KeyValue, global, trace::TracerProvider}; use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; use opentelemetry_otlp::WithExportConfig; use opentelemetry_sdk::{ - Resource, - logs::SdkLoggerProvider, - metrics::SdkMeterProvider, - trace::SdkTracerProvider, + Resource, logs::SdkLoggerProvider, metrics::SdkMeterProvider, trace::SdkTracerProvider, }; use tracing::warn; use tracing_subscriber::{ @@ -54,9 +51,7 @@ pub fn init_tracing(default_filter: &str, otel_config: OtelConfig) -> Result<(), tracing_opentelemetry::layer() .with_tracer(otel.tracer_provider.tracer("genarrative-api")), ) - .with( - OpenTelemetryTracingBridge::new(&otel.logger_provider).with_filter(LevelFilter::INFO), - ) + .with(OpenTelemetryTracingBridge::new(&otel.logger_provider).with_filter(LevelFilter::INFO)) .try_init() .map_err(|error| io::Error::other(format!("初始化 tracing subscriber 失败:{error}"))) } @@ -127,10 +122,12 @@ fn build_otel_pipeline() -> Option { .with_periodic_exporter(metric_exporter) .build(); let logger_provider = SdkLoggerProvider::builder() - .with_resource(Resource::builder() - .with_service_name(read_env_or_default("OTEL_SERVICE_NAME", "genarrative-api")) - .with_attribute(KeyValue::new("service.namespace", "genarrative")) - .build()) + .with_resource( + Resource::builder() + .with_service_name(read_env_or_default("OTEL_SERVICE_NAME", "genarrative-api")) + .with_attribute(KeyValue::new("service.namespace", "genarrative")) + .build(), + ) .with_batch_exporter(log_exporter) .build(); diff --git a/server-rs/crates/spacetime-client/Cargo.toml b/server-rs/crates/spacetime-client/Cargo.toml index 734c0df9..95c172ef 100644 --- a/server-rs/crates/spacetime-client/Cargo.toml +++ b/server-rs/crates/spacetime-client/Cargo.toml @@ -11,6 +11,7 @@ module-big-fish = { workspace = true } module-combat = { workspace = true } module-custom-world = { workspace = true } module-inventory = { workspace = true } +module-jump-hop = { workspace = true } module-match3d = { workspace = true } module-npc = { workspace = true } module-puzzle = { workspace = true } diff --git a/server-rs/crates/spacetime-client/src/assets.rs b/server-rs/crates/spacetime-client/src/assets.rs index 4cb2c2b7..f8814228 100644 --- a/server-rs/crates/spacetime-client/src/assets.rs +++ b/server-rs/crates/spacetime-client/src/assets.rs @@ -46,6 +46,21 @@ impl SpacetimeClient { .await } + pub async fn get_asset_object( + &self, + asset_object_id: String, + ) -> Result, SpacetimeClientError> { + self.read_after_connect("get_asset_object", move |connection| { + Ok(connection + .db() + .asset_object() + .asset_object_id() + .find(&asset_object_id) + .map(map_asset_object_row)) + }) + .await + } + pub async fn bind_asset_object_to_entity( &self, input: module_assets::AssetEntityBindingInput, diff --git a/server-rs/crates/spacetime-client/src/jump_hop.rs b/server-rs/crates/spacetime-client/src/jump_hop.rs new file mode 100644 index 00000000..7d798b88 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/jump_hop.rs @@ -0,0 +1,1149 @@ +use super::*; +use crate::mapper::{ + map_jump_hop_agent_session_procedure_result, map_jump_hop_gallery_card_view_row, + map_jump_hop_run_procedure_result, map_jump_hop_work_procedure_result, + map_jump_hop_works_procedure_result, +}; +use shared_contracts::jump_hop::{ + JumpHopActionRequest, JumpHopActionResponse, JumpHopActionType, JumpHopCharacterAsset, + JumpHopDifficulty, JumpHopDraftResponse, JumpHopGalleryResponse, JumpHopGenerationStatus, + JumpHopJumpRequest, JumpHopRestartRunRequest, JumpHopRuntimeRunSnapshotResponse, + JumpHopSessionSnapshotResponse, JumpHopStartRunRequest, JumpHopStylePreset, JumpHopTileAsset, + JumpHopTileType, JumpHopWorkProfileResponse, +}; +use shared_kernel::build_prefixed_uuid_id; + +const JUMP_HOP_TEMPLATE_ID: &str = "jump-hop"; +const JUMP_HOP_TEMPLATE_NAME: &str = "跳一跳"; + +impl SpacetimeClient { + pub async fn create_jump_hop_session( + &self, + session: JumpHopSessionSnapshotResponse, + ) -> Result { + let draft = session.draft.clone().ok_or_else(|| { + SpacetimeClientError::validation_failed("jump-hop session 缺少 draft") + })?; + let theme_tags_json = Some(json_string(&draft.theme_tags)?); + let config_json = Some(build_config_json(&draft)?); + let work_title = draft.work_title.clone(); + let work_description = draft.work_description.clone(); + let procedure_input = JumpHopAgentSessionCreateInput { + session_id: session.session_id, + owner_user_id: session.owner_user_id, + seed_text: work_title.clone(), + work_title, + work_description, + theme_tags_json, + welcome_message_text: "跳一跳草稿已准备好。".to_string(), + config_json, + created_at_micros: current_unix_micros(), + }; + + self.call_after_connect( + "create_jump_hop_agent_session", + move |connection, sender| { + connection.procedures().create_jump_hop_agent_session_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_jump_hop_agent_session_procedure_result); + send_once(&sender, mapped); + }, + ); + }, + ) + .await + } + + pub async fn get_jump_hop_session( + &self, + session_id: String, + owner_user_id: String, + ) -> Result { + let procedure_input = JumpHopAgentSessionGetInput { + session_id, + owner_user_id, + }; + + self.call_after_connect("get_jump_hop_agent_session", move |connection, sender| { + connection.procedures().get_jump_hop_agent_session_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_jump_hop_agent_session_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn execute_jump_hop_action( + &self, + session_id: String, + owner_user_id: String, + payload: JumpHopActionRequest, + ) -> Result { + let current = self + .get_jump_hop_session(session_id.clone(), owner_user_id.clone()) + .await?; + let (procedure, _) = + build_jump_hop_action_plan(¤t, &owner_user_id, &payload, current_unix_micros())?; + let (session, work) = match procedure { + JumpHopActionProcedure::Compile(input) => { + let profile_id = input.profile_id.clone(); + let session = self.compile_jump_hop_draft(input).await?; + let work = self + .get_jump_hop_work_profile(profile_id, owner_user_id) + .await + .ok(); + (session, work) + } + JumpHopActionProcedure::Update(input) => { + let work = self.update_jump_hop_work(input).await?; + let session = apply_jump_hop_work_to_session(current, &work); + (session, Some(work)) + } + }; + + Ok(JumpHopActionResponse { + action_type: payload.action_type, + session, + work, + }) + } + + pub async fn compile_jump_hop_draft( + &self, + procedure_input: JumpHopDraftCompileInput, + ) -> Result { + self.call_after_connect("compile_jump_hop_draft", move |connection, sender| { + connection.procedures().compile_jump_hop_draft_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_jump_hop_agent_session_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn get_jump_hop_work_profile( + &self, + profile_id: String, + owner_user_id: String, + ) -> Result { + let procedure_input = JumpHopWorkGetInput { + profile_id, + owner_user_id, + }; + + self.call_after_connect("get_jump_hop_work_profile", move |connection, sender| { + connection.procedures().get_jump_hop_work_profile_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_jump_hop_work_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn update_jump_hop_work( + &self, + procedure_input: JumpHopWorkUpdateInput, + ) -> Result { + self.call_after_connect("update_jump_hop_work", move |connection, sender| { + connection + .procedures() + .update_jump_hop_work_then(procedure_input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_jump_hop_work_procedure_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn publish_jump_hop_work( + &self, + profile_id: String, + owner_user_id: String, + ) -> Result { + let procedure_input = JumpHopWorkPublishInput { + profile_id, + owner_user_id, + published_at_micros: current_unix_micros(), + }; + + self.call_after_connect("publish_jump_hop_work", move |connection, sender| { + connection.procedures().publish_jump_hop_work_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_jump_hop_work_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn list_jump_hop_works( + &self, + owner_user_id: String, + ) -> Result, SpacetimeClientError> { + let procedure_input = JumpHopWorksListInput { + owner_user_id, + published_only: false, + }; + + self.call_after_connect("list_jump_hop_works", move |connection, sender| { + connection + .procedures() + .list_jump_hop_works_then(procedure_input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_jump_hop_works_procedure_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn get_jump_hop_runtime_work( + &self, + profile_id: String, + ) -> Result { + self.get_jump_hop_work_profile(profile_id, String::new()) + .await + } + + pub async fn start_jump_hop_run( + &self, + payload: JumpHopStartRunRequest, + owner_user_id: String, + ) -> Result { + let run_id = build_prefixed_uuid_id("jump-hop-run-"); + let procedure_input = JumpHopRunStartInput { + client_event_id: format!("{run_id}:start"), + run_id, + owner_user_id, + profile_id: payload.profile_id, + started_at_ms: current_unix_micros().div_euclid(1000), + }; + self.start_jump_hop_run_with_input(procedure_input).await + } + + pub async fn start_jump_hop_run_with_input( + &self, + procedure_input: JumpHopRunStartInput, + ) -> Result { + self.call_after_connect("start_jump_hop_run", move |connection, sender| { + connection + .procedures() + .start_jump_hop_run_then(procedure_input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_jump_hop_run_procedure_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn get_jump_hop_run( + &self, + run_id: String, + owner_user_id: String, + ) -> Result { + let procedure_input = JumpHopRunGetInput { + run_id, + owner_user_id, + }; + + self.call_after_connect("get_jump_hop_run", move |connection, sender| { + connection + .procedures() + .get_jump_hop_run_then(procedure_input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_jump_hop_run_procedure_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn jump_hop_run_jump( + &self, + run_id: String, + owner_user_id: String, + payload: JumpHopJumpRequest, + ) -> Result { + let procedure_input = JumpHopRunJumpInput { + run_id, + owner_user_id, + charge_ms: payload.charge_ms, + client_event_id: payload.client_event_id, + jumped_at_ms: current_unix_micros().div_euclid(1000), + }; + + self.call_after_connect("jump_hop_jump", move |connection, sender| { + connection + .procedures() + .jump_hop_jump_then(procedure_input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_jump_hop_run_procedure_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn restart_jump_hop_run( + &self, + run_id: String, + owner_user_id: String, + payload: JumpHopRestartRunRequest, + ) -> Result { + let procedure_input = JumpHopRunRestartInput { + source_run_id: run_id, + next_run_id: build_prefixed_uuid_id("jump-hop-run-"), + owner_user_id, + client_action_id: payload.client_action_id, + restarted_at_ms: current_unix_micros().div_euclid(1000), + }; + + self.call_after_connect("restart_jump_hop_run", move |connection, sender| { + connection + .procedures() + .restart_jump_hop_run_then(procedure_input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_jump_hop_run_procedure_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn list_jump_hop_gallery( + &self, + ) -> Result { + self.read_after_connect("list_jump_hop_gallery", move |connection| { + let mut items = connection + .db() + .jump_hop_gallery_card_view() + .iter() + .collect::>(); + items.sort_by(|left, right| { + right + .updated_at_micros + .cmp(&left.updated_at_micros) + .then_with(|| left.profile_id.cmp(&right.profile_id)) + }); + + Ok(JumpHopGalleryResponse { + items: items + .into_iter() + .map(map_jump_hop_gallery_card_view_row) + .collect(), + has_more: false, + next_cursor: None, + }) + }) + .await + } + + pub async fn get_jump_hop_gallery_detail( + &self, + public_work_code: String, + ) -> Result { + self.get_jump_hop_work_profile(public_work_code, String::new()) + .await + } +} + +enum JumpHopActionProcedure { + Compile(JumpHopDraftCompileInput), + Update(JumpHopWorkUpdateInput), +} + +#[derive(Clone, Copy)] +enum JumpHopDraftMergeScope { + CompileDraft, + RegenerateCharacter, + RegenerateTiles, + UpdateWorkMeta, + UpdateDifficulty, +} + +#[derive(Clone, Copy)] +enum JumpHopAssetRefresh { + Preserve, + Character, + Tiles, +} + +fn build_jump_hop_action_plan( + current: &JumpHopSessionSnapshotResponse, + owner_user_id: &str, + payload: &JumpHopActionRequest, + now_micros: i64, +) -> Result<(JumpHopActionProcedure, JumpHopDraftResponse), SpacetimeClientError> { + let scope = match payload.action_type { + JumpHopActionType::CompileDraft => JumpHopDraftMergeScope::CompileDraft, + JumpHopActionType::RegenerateCharacter => JumpHopDraftMergeScope::RegenerateCharacter, + JumpHopActionType::RegenerateTiles => JumpHopDraftMergeScope::RegenerateTiles, + JumpHopActionType::UpdateWorkMeta => JumpHopDraftMergeScope::UpdateWorkMeta, + JumpHopActionType::UpdateDifficulty => JumpHopDraftMergeScope::UpdateDifficulty, + }; + let mut draft = merge_action_into_draft(current.draft.clone(), payload, scope)?; + let profile_id = resolve_jump_hop_profile_id(&draft, &payload.action_type)?; + draft.profile_id = Some(profile_id.clone()); + + let procedure = match payload.action_type { + JumpHopActionType::CompileDraft => JumpHopActionProcedure::Compile(build_compile_input( + current, + owner_user_id, + &profile_id, + &mut draft, + JumpHopAssetRefresh::Preserve, + now_micros, + )?), + JumpHopActionType::RegenerateCharacter => { + JumpHopActionProcedure::Compile(build_compile_input( + current, + owner_user_id, + &profile_id, + &mut draft, + JumpHopAssetRefresh::Character, + now_micros, + )?) + } + JumpHopActionType::RegenerateTiles => JumpHopActionProcedure::Compile(build_compile_input( + current, + owner_user_id, + &profile_id, + &mut draft, + JumpHopAssetRefresh::Tiles, + now_micros, + )?), + JumpHopActionType::UpdateWorkMeta | JumpHopActionType::UpdateDifficulty => { + JumpHopActionProcedure::Update(build_update_input( + owner_user_id, + &profile_id, + &draft, + &payload.action_type, + now_micros, + )?) + } + }; + + Ok((procedure, draft)) +} + +fn merge_action_into_draft( + draft: Option, + payload: &JumpHopActionRequest, + scope: JumpHopDraftMergeScope, +) -> Result { + let mut draft = draft.unwrap_or_else(default_draft); + if matches!( + scope, + JumpHopDraftMergeScope::CompileDraft | JumpHopDraftMergeScope::UpdateWorkMeta + ) { + if let Some(value) = payload + .work_title + .as_ref() + .filter(|value| !value.trim().is_empty()) + { + draft.work_title = value.trim().to_string(); + } + if let Some(value) = payload.work_description.as_ref() { + draft.work_description = value.trim().to_string(); + } + if let Some(value) = payload.theme_tags.clone() { + draft.theme_tags = normalize_jump_hop_tags(value); + } + } + if matches!( + scope, + JumpHopDraftMergeScope::CompileDraft | JumpHopDraftMergeScope::UpdateDifficulty + ) { + if let Some(value) = payload.difficulty.clone() { + draft.difficulty = value; + } + } + if matches!(scope, JumpHopDraftMergeScope::CompileDraft) { + if let Some(value) = payload.style_preset.clone() { + draft.style_preset = value; + } + if payload.end_mood_prompt.is_some() { + draft.end_mood_prompt = payload + .end_mood_prompt + .as_ref() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()); + } + } + if matches!( + scope, + JumpHopDraftMergeScope::CompileDraft | JumpHopDraftMergeScope::RegenerateCharacter + ) && let Some(value) = payload + .character_prompt + .as_ref() + .filter(|value| !value.trim().is_empty()) + { + draft.character_prompt = value.trim().to_string(); + } + if matches!( + scope, + JumpHopDraftMergeScope::CompileDraft | JumpHopDraftMergeScope::RegenerateTiles + ) && let Some(value) = payload + .tile_prompt + .as_ref() + .filter(|value| !value.trim().is_empty()) + { + draft.tile_prompt = value.trim().to_string(); + } + if draft.work_title.trim().is_empty() { + return Err(SpacetimeClientError::validation_failed( + "jump-hop work_title 不能为空", + )); + } + Ok(draft) +} + +fn build_compile_input( + current: &JumpHopSessionSnapshotResponse, + owner_user_id: &str, + profile_id: &str, + draft: &mut JumpHopDraftResponse, + refresh: JumpHopAssetRefresh, + now_micros: i64, +) -> Result { + let force_character = matches!(refresh, JumpHopAssetRefresh::Character); + let force_tiles = matches!(refresh, JumpHopAssetRefresh::Tiles); + if force_character { + draft.character_asset = None; + } + if force_tiles { + draft.tile_atlas_asset = None; + draft.tile_assets.clear(); + } + let character_asset = ensure_character_asset( + draft.character_asset.clone(), + profile_id, + &draft.character_prompt, + force_character, + now_micros, + ); + let tile_atlas_asset = ensure_tile_atlas_asset( + draft.tile_atlas_asset.clone(), + profile_id, + &draft.tile_prompt, + force_tiles, + now_micros, + ); + let tile_assets = ensure_tile_assets( + draft.tile_assets.clone(), + profile_id, + force_tiles, + now_micros, + ); + let cover_composite = resolve_cover_composite(draft, profile_id, refresh, now_micros); + + draft.character_asset = Some(character_asset.clone()); + draft.tile_atlas_asset = Some(tile_atlas_asset.clone()); + draft.tile_assets = tile_assets.clone(); + draft.cover_composite = cover_composite.clone(); + draft.generation_status = JumpHopGenerationStatus::Ready; + + Ok(JumpHopDraftCompileInput { + session_id: current.session_id.clone(), + owner_user_id: owner_user_id.to_string(), + profile_id: profile_id.to_string(), + author_display_name: "跳一跳玩家".to_string(), + seed_text: draft.work_title.clone(), + work_title: draft.work_title.clone(), + work_description: draft.work_description.clone(), + theme_tags_json: Some(json_string(&draft.theme_tags)?), + theme_text: Some(draft.work_title.clone()), + difficulty: Some(difficulty_to_str(&draft.difficulty).to_string()), + style_preset: Some(style_to_str(&draft.style_preset).to_string()), + character_prompt: Some(draft.character_prompt.clone()), + tile_prompt: Some(draft.tile_prompt.clone()), + end_mood_prompt: draft.end_mood_prompt.clone(), + character_asset_json: Some(json_string(&character_asset)?), + tile_atlas_asset_json: Some(json_string(&tile_atlas_asset)?), + tile_assets_json: Some(json_string(&tile_assets)?), + cover_composite, + generation_status: Some("ready".to_string()), + compiled_at_micros: now_micros, + }) +} + +fn build_update_input( + owner_user_id: &str, + profile_id: &str, + draft: &JumpHopDraftResponse, + action_type: &JumpHopActionType, + now_micros: i64, +) -> Result { + Ok(JumpHopWorkUpdateInput { + profile_id: profile_id.to_string(), + owner_user_id: owner_user_id.to_string(), + work_title: draft.work_title.clone(), + work_description: draft.work_description.clone(), + theme_tags_json: json_string(&draft.theme_tags)?, + difficulty: matches!(action_type, JumpHopActionType::UpdateDifficulty) + .then(|| difficulty_to_str(&draft.difficulty).to_string()), + style_preset: None, + cover_image_src: None, + cover_composite: None, + updated_at_micros: now_micros, + }) +} + +fn resolve_jump_hop_profile_id( + draft: &JumpHopDraftResponse, + action_type: &JumpHopActionType, +) -> Result { + if let Some(profile_id) = draft + .profile_id + .as_ref() + .map(|value| value.trim()) + .filter(|value| !value.is_empty()) + { + return Ok(profile_id.to_string()); + } + if matches!(action_type, JumpHopActionType::CompileDraft) { + return Ok(build_prefixed_uuid_id("jump-hop-profile-")); + } + Err(SpacetimeClientError::validation_failed( + "jump-hop action 需要先完成 compile-draft", + )) +} + +fn apply_jump_hop_work_to_session( + mut session: JumpHopSessionSnapshotResponse, + work: &JumpHopWorkProfileResponse, +) -> JumpHopSessionSnapshotResponse { + session.status = work.draft.generation_status.clone(); + session.draft = Some(work.draft.clone()); + session.updated_at = work.summary.updated_at.clone(); + session +} + +fn normalize_jump_hop_tags(tags: Vec) -> Vec { + tags.into_iter() + .map(|tag| tag.trim().to_string()) + .filter(|tag| !tag.is_empty()) + .take(8) + .collect() +} + +fn default_draft() -> JumpHopDraftResponse { + JumpHopDraftResponse { + template_id: JUMP_HOP_TEMPLATE_ID.to_string(), + template_name: JUMP_HOP_TEMPLATE_NAME.to_string(), + profile_id: None, + work_title: JUMP_HOP_TEMPLATE_NAME.to_string(), + work_description: "俯视角跳跃闯关".to_string(), + theme_tags: vec!["跳一跳".to_string(), "休闲".to_string()], + difficulty: JumpHopDifficulty::Standard, + style_preset: JumpHopStylePreset::MinimalBlocks, + character_prompt: "俯视角可爱主角,透明背景".to_string(), + tile_prompt: "等距立体地块图集".to_string(), + end_mood_prompt: None, + character_asset: None, + tile_atlas_asset: None, + tile_assets: Vec::new(), + path: None, + cover_composite: None, + generation_status: JumpHopGenerationStatus::Draft, + } +} + +fn build_config_json(draft: &JumpHopDraftResponse) -> Result { + serde_json::to_string(&serde_json::json!({ + "themeText": draft.work_title, + "difficulty": difficulty_to_str(&draft.difficulty), + "stylePreset": style_to_str(&draft.style_preset), + "characterPrompt": draft.character_prompt, + "tilePrompt": draft.tile_prompt, + "endMoodPrompt": draft.end_mood_prompt.clone().unwrap_or_default(), + })) + .map_err(SpacetimeClientError::validation_failed) +} + +fn ensure_character_asset( + existing: Option, + profile_id: &str, + prompt: &str, + force_new: bool, + now_micros: i64, +) -> JumpHopCharacterAsset { + if !force_new && let Some(asset) = existing { + return asset; + } + let revision = force_new.then_some(now_micros); + let suffix = asset_revision_suffix(revision); + JumpHopCharacterAsset { + asset_id: format!("{profile_id}-character{suffix}"), + image_src: format!("/generated-jump-hop-assets/{profile_id}/character{suffix}.png"), + image_object_key: format!("generated-jump-hop-assets/{profile_id}/character{suffix}.png"), + asset_object_id: format!("{profile_id}-character{suffix}-object"), + generation_provider: "deterministic-placeholder".to_string(), + prompt: prompt.to_string(), + width: 768, + height: 768, + } +} + +fn ensure_tile_atlas_asset( + existing: Option, + profile_id: &str, + prompt: &str, + force_new: bool, + now_micros: i64, +) -> JumpHopCharacterAsset { + if !force_new && let Some(asset) = existing { + return asset; + } + let revision = force_new.then_some(now_micros); + let suffix = asset_revision_suffix(revision); + JumpHopCharacterAsset { + asset_id: format!("{profile_id}-tile-atlas{suffix}"), + image_src: format!("/generated-jump-hop-assets/{profile_id}/tile-atlas{suffix}.png"), + image_object_key: format!("generated-jump-hop-assets/{profile_id}/tile-atlas{suffix}.png"), + asset_object_id: format!("{profile_id}-tile-atlas{suffix}-object"), + generation_provider: "deterministic-placeholder".to_string(), + prompt: prompt.to_string(), + width: 1024, + height: 1024, + } +} + +fn ensure_tile_assets( + existing: Vec, + profile_id: &str, + force_new: bool, + now_micros: i64, +) -> Vec { + if !force_new && !existing.is_empty() { + return existing; + } + let suffix = asset_revision_suffix(force_new.then_some(now_micros)); + [ + JumpHopTileType::Start, + JumpHopTileType::Normal, + JumpHopTileType::Target, + JumpHopTileType::Finish, + JumpHopTileType::Bonus, + JumpHopTileType::Accent, + ] + .into_iter() + .enumerate() + .map(|(index, tile_type)| JumpHopTileAsset { + tile_type, + image_src: format!("/generated-jump-hop-assets/{profile_id}/tiles/{index}{suffix}.png"), + image_object_key: format!( + "generated-jump-hop-assets/{profile_id}/tiles/{index}{suffix}.png" + ), + asset_object_id: format!("{profile_id}-tile-{index}{suffix}-object"), + source_atlas_cell: format!("cell-{index}{suffix}"), + visual_width: 256, + visual_height: 192, + top_surface_radius: 42.0, + landing_radius: 34.0, + }) + .collect() +} + +fn resolve_cover_composite( + draft: &JumpHopDraftResponse, + profile_id: &str, + refresh: JumpHopAssetRefresh, + now_micros: i64, +) -> Option { + if matches!(refresh, JumpHopAssetRefresh::Preserve) + && let Some(value) = draft + .cover_composite + .as_ref() + .map(|value| value.trim()) + .filter(|value| !value.is_empty()) + { + return Some(value.to_string()); + } + let suffix = asset_revision_suffix( + (!matches!(refresh, JumpHopAssetRefresh::Preserve)).then_some(now_micros), + ); + Some(format!( + "/generated-jump-hop-assets/{profile_id}/cover-composite{suffix}.png" + )) +} + +fn asset_revision_suffix(revision: Option) -> String { + revision + .filter(|value| *value > 0) + .map(|value| format!("-{value}")) + .unwrap_or_default() +} + +fn json_string(value: &T) -> Result { + serde_json::to_string(value).map_err(SpacetimeClientError::validation_failed) +} + +fn difficulty_to_str(value: &JumpHopDifficulty) -> &'static str { + match value { + JumpHopDifficulty::Easy => "easy", + JumpHopDifficulty::Standard => "standard", + JumpHopDifficulty::Advanced => "advanced", + JumpHopDifficulty::Challenge => "challenge", + } +} + +fn style_to_str(value: &JumpHopStylePreset) -> &'static str { + match value { + JumpHopStylePreset::MinimalBlocks => "minimal-blocks", + JumpHopStylePreset::PaperToy => "paper-toy", + JumpHopStylePreset::NeonGlass => "neon-glass", + JumpHopStylePreset::ForestStone => "forest-stone", + JumpHopStylePreset::FutureMetal => "future-metal", + JumpHopStylePreset::Custom => "custom", + } +} + +#[cfg(test)] +mod tests { + use super::*; + use shared_contracts::jump_hop::JumpHopActionType; + + const SESSION_ID: &str = "jump-hop-session-test"; + const OWNER_USER_ID: &str = "user-test"; + const PROFILE_ID: &str = "jump-hop-profile-test"; + const NOW_MICROS: i64 = 1_763_456_789_000_000; + + #[test] + fn jump_hop_action_compile_draft_builds_compile_input_with_assets() { + let session = session_with_draft(draft_without_assets()); + let payload = action(JumpHopActionType::CompileDraft); + + let (plan, draft) = + build_jump_hop_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) + .expect("compile-draft should build plan"); + + let JumpHopActionProcedure::Compile(input) = plan else { + panic!("compile-draft should call compile_jump_hop_draft"); + }; + assert_eq!(input.session_id, SESSION_ID); + assert_eq!(input.owner_user_id, OWNER_USER_ID); + assert_eq!(input.generation_status.as_deref(), Some("ready")); + assert!( + input + .character_asset_json + .as_deref() + .unwrap_or("") + .contains("-character") + ); + assert!( + input + .tile_atlas_asset_json + .as_deref() + .unwrap_or("") + .contains("-tile-atlas") + ); + assert!( + input + .tile_assets_json + .as_deref() + .unwrap_or("") + .contains("tile-0-object") + ); + assert_eq!(draft.generation_status, JumpHopGenerationStatus::Ready); + } + + #[test] + fn jump_hop_action_regenerate_character_replaces_only_character_asset_input() { + let session = session_with_draft(draft_with_assets()); + let mut payload = action(JumpHopActionType::RegenerateCharacter); + payload.character_prompt = Some("新的主角提示词".to_string()); + + let (plan, _draft) = + build_jump_hop_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) + .expect("regenerate-character should build plan"); + + let JumpHopActionProcedure::Compile(input) = plan else { + panic!("regenerate-character should call compile_jump_hop_draft"); + }; + assert!( + !input + .character_asset_json + .as_deref() + .unwrap_or("") + .contains("old-character") + ); + assert!( + input + .character_asset_json + .as_deref() + .unwrap_or("") + .contains(&NOW_MICROS.to_string()) + ); + assert!( + input + .tile_atlas_asset_json + .as_deref() + .unwrap_or("") + .contains("old-tile-atlas") + ); + assert!( + input + .tile_assets_json + .as_deref() + .unwrap_or("") + .contains("old-normal-tile") + ); + } + + #[test] + fn jump_hop_action_regenerate_tiles_replaces_only_tile_asset_input() { + let session = session_with_draft(draft_with_assets()); + let mut payload = action(JumpHopActionType::RegenerateTiles); + payload.tile_prompt = Some("新的地块提示词".to_string()); + + let (plan, _draft) = + build_jump_hop_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) + .expect("regenerate-tiles should build plan"); + + let JumpHopActionProcedure::Compile(input) = plan else { + panic!("regenerate-tiles should call compile_jump_hop_draft"); + }; + assert!( + input + .character_asset_json + .as_deref() + .unwrap_or("") + .contains("old-character") + ); + assert!( + !input + .tile_atlas_asset_json + .as_deref() + .unwrap_or("") + .contains("old-tile-atlas") + ); + assert!( + !input + .tile_assets_json + .as_deref() + .unwrap_or("") + .contains("old-normal-tile") + ); + assert!( + input + .tile_atlas_asset_json + .as_deref() + .unwrap_or("") + .contains(&NOW_MICROS.to_string()) + ); + assert!( + input + .tile_assets_json + .as_deref() + .unwrap_or("") + .contains(&NOW_MICROS.to_string()) + ); + } + + #[test] + fn jump_hop_action_update_work_meta_builds_update_input_without_asset_compile() { + let session = session_with_draft(draft_with_assets()); + let mut payload = action(JumpHopActionType::UpdateWorkMeta); + payload.work_title = Some("新标题".to_string()); + payload.work_description = Some("新描述".to_string()); + payload.theme_tags = Some(vec![" A ".to_string(), "B".to_string()]); + payload.character_prompt = Some("不应影响角色资产".to_string()); + + let (plan, draft) = + build_jump_hop_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) + .expect("update-work-meta should build plan"); + + let JumpHopActionProcedure::Update(input) = plan else { + panic!("update-work-meta should call update_jump_hop_work"); + }; + assert_eq!(input.profile_id, PROFILE_ID); + assert_eq!(input.work_title, "新标题"); + assert_eq!(input.work_description, "新描述"); + assert!(input.difficulty.is_none()); + assert!(input.style_preset.is_none()); + assert_eq!(draft.character_prompt, "旧角色提示词"); + } + + #[test] + fn jump_hop_action_update_difficulty_builds_update_input_without_asset_compile() { + let session = session_with_draft(draft_with_assets()); + let mut payload = action(JumpHopActionType::UpdateDifficulty); + payload.difficulty = Some(JumpHopDifficulty::Challenge); + + let (plan, draft) = + build_jump_hop_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS) + .expect("update-difficulty should build plan"); + + let JumpHopActionProcedure::Update(input) = plan else { + panic!("update-difficulty should call update_jump_hop_work"); + }; + assert_eq!(input.difficulty.as_deref(), Some("challenge")); + assert!(input.style_preset.is_none()); + assert_eq!( + draft + .character_asset + .as_ref() + .map(|asset| asset.asset_id.as_str()), + Some("old-character") + ); + assert_eq!( + draft + .tile_assets + .first() + .map(|asset| asset.asset_object_id.as_str()), + Some("old-normal-tile-object") + ); + } + + fn action(action_type: JumpHopActionType) -> JumpHopActionRequest { + JumpHopActionRequest { + action_type, + work_title: None, + work_description: None, + theme_tags: None, + difficulty: None, + style_preset: None, + character_prompt: None, + tile_prompt: None, + end_mood_prompt: None, + } + } + + fn session_with_draft(draft: JumpHopDraftResponse) -> JumpHopSessionSnapshotResponse { + JumpHopSessionSnapshotResponse { + session_id: SESSION_ID.to_string(), + owner_user_id: OWNER_USER_ID.to_string(), + status: draft.generation_status.clone(), + draft: Some(draft), + created_at: "2026-05-19T00:00:00Z".to_string(), + updated_at: "2026-05-19T00:00:00Z".to_string(), + } + } + + fn draft_without_assets() -> JumpHopDraftResponse { + JumpHopDraftResponse { + profile_id: None, + ..base_draft() + } + } + + fn draft_with_assets() -> JumpHopDraftResponse { + JumpHopDraftResponse { + profile_id: Some(PROFILE_ID.to_string()), + character_asset: Some(JumpHopCharacterAsset { + asset_id: "old-character".to_string(), + image_src: "/generated-jump-hop-assets/old-character.png".to_string(), + image_object_key: "generated-jump-hop-assets/old-character.png".to_string(), + asset_object_id: "old-character-object".to_string(), + generation_provider: "old-provider".to_string(), + prompt: "旧角色提示词".to_string(), + width: 768, + height: 768, + }), + tile_atlas_asset: Some(JumpHopCharacterAsset { + asset_id: "old-tile-atlas".to_string(), + image_src: "/generated-jump-hop-assets/old-tile-atlas.png".to_string(), + image_object_key: "generated-jump-hop-assets/old-tile-atlas.png".to_string(), + asset_object_id: "old-tile-atlas-object".to_string(), + generation_provider: "old-provider".to_string(), + prompt: "旧地块提示词".to_string(), + width: 1024, + height: 1024, + }), + tile_assets: vec![JumpHopTileAsset { + tile_type: JumpHopTileType::Normal, + image_src: "/generated-jump-hop-assets/old-normal-tile.png".to_string(), + image_object_key: "generated-jump-hop-assets/old-normal-tile.png".to_string(), + asset_object_id: "old-normal-tile-object".to_string(), + source_atlas_cell: "old-cell".to_string(), + visual_width: 256, + visual_height: 192, + top_surface_radius: 42.0, + landing_radius: 34.0, + }], + path: Some(sample_jump_hop_path()), + cover_composite: Some("/generated-jump-hop-assets/old-cover.png".to_string()), + generation_status: JumpHopGenerationStatus::Ready, + ..base_draft() + } + } + + fn base_draft() -> JumpHopDraftResponse { + JumpHopDraftResponse { + template_id: JUMP_HOP_TEMPLATE_ID.to_string(), + template_name: JUMP_HOP_TEMPLATE_NAME.to_string(), + profile_id: None, + work_title: "旧标题".to_string(), + work_description: "旧描述".to_string(), + theme_tags: vec!["旧标签".to_string()], + difficulty: JumpHopDifficulty::Standard, + style_preset: JumpHopStylePreset::MinimalBlocks, + character_prompt: "旧角色提示词".to_string(), + tile_prompt: "旧地块提示词".to_string(), + end_mood_prompt: None, + character_asset: None, + tile_atlas_asset: None, + tile_assets: Vec::new(), + path: None, + cover_composite: None, + generation_status: JumpHopGenerationStatus::Draft, + } + } + + fn sample_jump_hop_path() -> JumpHopPath { + JumpHopPath { + seed: "jump-hop-test".to_string(), + difficulty: JumpHopDifficulty::Standard, + platforms: vec![JumpHopPlatform { + platform_id: "platform-0".to_string(), + tile_type: JumpHopTileType::Start, + x: 0.0, + y: 0.0, + width: 92.0, + height: 70.0, + landing_radius: 34.0, + perfect_radius: 14.0, + score_value: 10, + }], + finish_index: 0, + camera_preset: "portrait-isometric-follow".to_string(), + scoring: JumpHopScoring { + charge_to_distance_ratio: 0.018, + max_charge_ms: 1_200, + hit_bonus: 10, + perfect_bonus: 20, + }, + } + } +} diff --git a/server-rs/crates/spacetime-client/src/lib.rs b/server-rs/crates/spacetime-client/src/lib.rs index 4b39be01..fb492293 100644 --- a/server-rs/crates/spacetime-client/src/lib.rs +++ b/server-rs/crates/spacetime-client/src/lib.rs @@ -30,7 +30,16 @@ pub use mapper::{ CustomWorldPublishGateRecord, CustomWorldPublishWorldRecord, CustomWorldPublishWorldRecordInput, CustomWorldPublishedProfileCompileRecord, CustomWorldResultPreviewBlockerRecord, CustomWorldSupportedActionRecord, - CustomWorldWorkSummaryRecord, Match3DAgentMessageFinalizeRecordInput, + CustomWorldWorkSummaryRecord, JumpHopActionRequest, JumpHopActionResponse, JumpHopActionType, + JumpHopCharacterAsset, JumpHopDifficulty, JumpHopDraftResponse, JumpHopGalleryCardResponse, + JumpHopGalleryDetailResponse, JumpHopGalleryResponse, JumpHopGenerationStatus, + JumpHopJumpRequest, JumpHopJumpResponse, JumpHopJumpResult, JumpHopLastJump, JumpHopPath, + JumpHopPlatform, JumpHopRestartRunRequest, JumpHopRunResponse, JumpHopRunStatus, + JumpHopRuntimeRunSnapshotResponse, JumpHopScoring, JumpHopSessionResponse, + JumpHopSessionSnapshotResponse, JumpHopStartRunRequest, JumpHopStylePreset, JumpHopTileAsset, + JumpHopTileType, JumpHopWorkDetailResponse, JumpHopWorkMutationResponse, + JumpHopWorkProfileResponse, JumpHopWorkSummaryResponse, JumpHopWorksResponse, + JumpHopWorkspaceCreateRequest, Match3DAgentMessageFinalizeRecordInput, Match3DAgentMessageRecord, Match3DAgentMessageSubmitRecordInput, Match3DAgentSessionCreateRecordInput, Match3DAgentSessionRecord, Match3DAnchorItemRecord, Match3DAnchorPackRecord, Match3DClickConfirmationRecord, Match3DCompileDraftRecordInput, @@ -86,6 +95,7 @@ pub mod big_fish; pub mod combat; pub mod custom_world; pub mod inventory; +pub mod jump_hop; pub mod match3d; pub mod npc; pub mod puzzle; @@ -552,6 +562,7 @@ impl SpacetimeClient { for query in [ "SELECT * FROM bark_battle_gallery_view", "SELECT * FROM puzzle_gallery_card_view", + "SELECT * FROM jump_hop_gallery_card_view", "SELECT * FROM custom_world_gallery_entry", "SELECT * FROM match_3_d_gallery_view", "SELECT * FROM square_hole_gallery_view", @@ -566,6 +577,7 @@ impl SpacetimeClient { for query in [ "SELECT * FROM public_work_play_daily_stat WHERE source_type = 'puzzle'", + "SELECT * FROM public_work_play_daily_stat WHERE source_type = 'jump-hop'", "SELECT * FROM public_work_play_daily_stat WHERE source_type = 'custom-world'", "SELECT * FROM public_work_play_daily_stat WHERE source_type = 'match3d'", "SELECT * FROM public_work_play_daily_stat WHERE source_type = 'square-hole'", @@ -574,6 +586,7 @@ impl SpacetimeClient { "SELECT * FROM public_work_play_daily_stat WHERE source_type = 'bark-battle'", "SELECT * FROM creation_entry_config", "SELECT * FROM creation_entry_type_config", + "SELECT * FROM asset_object", ] { if let Ok(subscription) = self .subscribe_cached_read_model_query(connection, broken.clone(), query, false) diff --git a/server-rs/crates/spacetime-client/src/mapper.rs b/server-rs/crates/spacetime-client/src/mapper.rs index 700a8b8f..f2bc9530 100644 --- a/server-rs/crates/spacetime-client/src/mapper.rs +++ b/server-rs/crates/spacetime-client/src/mapper.rs @@ -9,6 +9,7 @@ mod combat; mod common; mod custom_world; mod inventory; +mod jump_hop; mod match3d; mod npc; mod puzzle; @@ -64,6 +65,18 @@ pub use self::common::{ VisualNovelRunSnapshotRecordInput, VisualNovelRunStartRecordInput, VisualNovelWorkCompileRecordInput, }; +pub use self::jump_hop::{ + JumpHopActionRequest, JumpHopActionResponse, JumpHopActionType, JumpHopCharacterAsset, + JumpHopDifficulty, JumpHopDraftResponse, JumpHopGalleryCardResponse, + JumpHopGalleryDetailResponse, JumpHopGalleryResponse, JumpHopGenerationStatus, + JumpHopJumpRequest, JumpHopJumpResponse, JumpHopJumpResult, JumpHopLastJump, JumpHopPath, + JumpHopPlatform, JumpHopRestartRunRequest, JumpHopRunResponse, JumpHopRunStatus, + JumpHopRuntimeRunSnapshotResponse, JumpHopScoring, JumpHopSessionResponse, + JumpHopSessionSnapshotResponse, JumpHopStartRunRequest, JumpHopStylePreset, JumpHopTileAsset, + JumpHopTileType, JumpHopWorkDetailResponse, JumpHopWorkMutationResponse, + JumpHopWorkProfileResponse, JumpHopWorkSummaryResponse, JumpHopWorksResponse, + JumpHopWorkspaceCreateRequest, +}; pub use self::match3d::{ Match3DAgentMessageFinalizeRecordInput, Match3DAgentMessageRecord, Match3DAgentMessageSubmitRecordInput, Match3DAgentSessionCreateRecordInput, @@ -107,7 +120,9 @@ pub use self::runtime_profile::{ pub use self::story::{VisualNovelRuntimeEventRecord, VisualNovelRuntimeEventRecordInput}; pub(crate) use self::ai::map_ai_task_procedure_result; -pub(crate) use self::assets::{map_entity_binding_procedure_result, map_procedure_result}; +pub(crate) use self::assets::{ + map_asset_object_row, map_entity_binding_procedure_result, map_procedure_result, +}; pub(crate) use self::auth::{ map_auth_store_snapshot_import_procedure_result, map_auth_store_snapshot_procedure_result, }; @@ -140,6 +155,11 @@ pub(crate) use self::inventory::{ map_runtime_inventory_state_procedure_result, map_runtime_item_reward_item_snapshot, map_runtime_item_reward_item_snapshot_back, }; +pub(crate) use self::jump_hop::{ + map_jump_hop_agent_session_procedure_result, map_jump_hop_gallery_card_view_row, + map_jump_hop_run_procedure_result, map_jump_hop_work_procedure_result, + map_jump_hop_works_procedure_result, +}; pub(crate) use self::match3d::{ map_match3d_agent_session_procedure_result, map_match3d_click_item_procedure_result, map_match3d_gallery_view_row, map_match3d_run_procedure_result, diff --git a/server-rs/crates/spacetime-client/src/mapper/assets.rs b/server-rs/crates/spacetime-client/src/mapper/assets.rs index 0e9586f3..8c46ab82 100644 --- a/server-rs/crates/spacetime-client/src/mapper/assets.rs +++ b/server-rs/crates/spacetime-client/src/mapper/assets.rs @@ -115,6 +115,26 @@ pub(crate) fn map_snapshot( } } +pub(crate) fn map_asset_object_row(row: AssetObject) -> AssetObjectRecord { + build_asset_object_record(module_assets::AssetObjectUpsertSnapshot { + asset_object_id: row.asset_object_id, + bucket: row.bucket, + object_key: row.object_key, + access_policy: map_access_policy_back(row.access_policy), + content_type: row.content_type, + content_length: row.content_length, + content_hash: row.content_hash, + version: row.version, + source_job_id: row.source_job_id, + owner_user_id: row.owner_user_id, + profile_id: row.profile_id, + entity_id: row.entity_id, + asset_kind: row.asset_kind, + created_at_micros: row.created_at.to_micros_since_unix_epoch(), + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + }) +} + pub(crate) fn map_access_policy( value: AssetObjectAccessPolicy, ) -> crate::module_bindings::AssetObjectAccessPolicy { diff --git a/server-rs/crates/spacetime-client/src/mapper/jump_hop.rs b/server-rs/crates/spacetime-client/src/mapper/jump_hop.rs new file mode 100644 index 00000000..a2384840 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/mapper/jump_hop.rs @@ -0,0 +1,354 @@ +use super::*; +pub use shared_contracts::jump_hop::{ + JumpHopActionRequest, JumpHopActionResponse, JumpHopActionType, JumpHopCharacterAsset, + JumpHopDifficulty, JumpHopDraftResponse, JumpHopGalleryCardResponse, + JumpHopGalleryDetailResponse, JumpHopGalleryResponse, JumpHopGenerationStatus, + JumpHopJumpRequest, JumpHopJumpResponse, JumpHopJumpResult, JumpHopLastJump, JumpHopPath, + JumpHopPlatform, JumpHopRestartRunRequest, JumpHopRunResponse, JumpHopRunStatus, + JumpHopRuntimeRunSnapshotResponse, JumpHopScoring, JumpHopSessionResponse, + JumpHopSessionSnapshotResponse, JumpHopStartRunRequest, JumpHopStylePreset, JumpHopTileAsset, + JumpHopTileType, JumpHopWorkDetailResponse, JumpHopWorkMutationResponse, + JumpHopWorkProfileResponse, JumpHopWorkSummaryResponse, JumpHopWorksResponse, + JumpHopWorkspaceCreateRequest, +}; + +pub(crate) fn map_jump_hop_agent_session_procedure_result( + result: JumpHopAgentSessionProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + let session = result + .session + .ok_or_else(|| SpacetimeClientError::missing_snapshot("jump hop agent session 快照"))?; + Ok(map_jump_hop_session_snapshot(session)) +} + +pub(crate) fn map_jump_hop_work_procedure_result( + result: JumpHopWorkProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + let work = result + .work + .ok_or_else(|| SpacetimeClientError::missing_snapshot("jump hop work 快照"))?; + map_jump_hop_work_snapshot(work) +} + +pub(crate) fn map_jump_hop_works_procedure_result( + result: JumpHopWorksProcedureResult, +) -> Result, SpacetimeClientError> { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + result + .items + .into_iter() + .map(map_jump_hop_work_snapshot) + .collect() +} + +pub(crate) fn map_jump_hop_run_procedure_result( + result: JumpHopRunProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + let run = result + .run + .ok_or_else(|| SpacetimeClientError::missing_snapshot("jump hop run 快照"))?; + Ok(map_jump_hop_run_snapshot(run)) +} + +pub(crate) fn map_jump_hop_gallery_card_view_row( + row: JumpHopGalleryCardViewRow, +) -> JumpHopGalleryCardResponse { + JumpHopGalleryCardResponse { + public_work_code: row.public_work_code, + work_id: row.work_id, + profile_id: row.profile_id, + owner_user_id: row.owner_user_id, + author_display_name: row.author_display_name, + work_title: row.work_title, + work_description: row.work_description, + cover_image_src: empty_string_to_none(row.cover_image_src), + theme_tags: row.theme_tags, + difficulty: parse_difficulty(&row.difficulty), + style_preset: parse_style_preset(&row.style_preset), + publication_status: normalize_publication_status(&row.publication_status).to_string(), + play_count: row.play_count, + updated_at: format_timestamp_micros(row.updated_at_micros), + published_at: row.published_at_micros.map(format_timestamp_micros), + generation_status: parse_generation_status(&row.generation_status), + } +} + +fn map_jump_hop_session_snapshot( + snapshot: JumpHopAgentSessionSnapshot, +) -> JumpHopSessionSnapshotResponse { + JumpHopSessionSnapshotResponse { + session_id: snapshot.session_id, + owner_user_id: snapshot.owner_user_id, + status: snapshot + .draft + .as_ref() + .map(|draft| parse_generation_status(&draft.generation_status)) + .unwrap_or(JumpHopGenerationStatus::Draft), + draft: snapshot.draft.map(map_jump_hop_draft_snapshot), + created_at: format_timestamp_micros(snapshot.created_at_micros), + updated_at: format_timestamp_micros(snapshot.updated_at_micros), + } +} + +fn map_jump_hop_work_snapshot( + snapshot: JumpHopWorkSnapshot, +) -> Result { + let draft = JumpHopDraftResponse { + template_id: "jump-hop".to_string(), + template_name: "跳一跳".to_string(), + profile_id: Some(snapshot.profile_id.clone()), + work_title: snapshot.work_title.clone(), + work_description: snapshot.work_description.clone(), + theme_tags: snapshot.theme_tags.clone(), + difficulty: parse_difficulty(&snapshot.difficulty), + style_preset: parse_style_preset(&snapshot.style_preset), + character_prompt: snapshot.character_prompt.clone(), + tile_prompt: snapshot.tile_prompt.clone(), + end_mood_prompt: snapshot.end_mood_prompt.clone(), + character_asset: snapshot.character_asset.clone().map(map_character_asset), + tile_atlas_asset: snapshot.tile_atlas_asset.clone().map(map_character_asset), + tile_assets: snapshot + .tile_assets + .clone() + .into_iter() + .map(map_tile_asset) + .collect(), + path: Some(map_jump_hop_path(snapshot.path.clone())), + cover_composite: snapshot.cover_composite.clone(), + generation_status: parse_generation_status(&snapshot.generation_status), + }; + let character_asset = draft + .character_asset + .clone() + .ok_or_else(|| SpacetimeClientError::missing_snapshot("jump hop character asset"))?; + let tile_atlas_asset = draft + .tile_atlas_asset + .clone() + .ok_or_else(|| SpacetimeClientError::missing_snapshot("jump hop tile atlas asset"))?; + Ok(JumpHopWorkProfileResponse { + summary: JumpHopWorkSummaryResponse { + runtime_kind: "jump-hop".to_string(), + work_id: snapshot.work_id, + profile_id: snapshot.profile_id, + owner_user_id: snapshot.owner_user_id, + source_session_id: empty_string_to_none(snapshot.source_session_id), + work_title: snapshot.work_title, + work_description: snapshot.work_description, + theme_tags: snapshot.theme_tags, + difficulty: parse_difficulty(&snapshot.difficulty), + style_preset: parse_style_preset(&snapshot.style_preset), + cover_image_src: empty_string_to_none(snapshot.cover_image_src), + publication_status: normalize_publication_status(&snapshot.publication_status) + .to_string(), + play_count: snapshot.play_count, + updated_at: format_timestamp_micros(snapshot.updated_at_micros), + published_at: snapshot.published_at_micros.map(format_timestamp_micros), + publish_ready: snapshot.publish_ready, + generation_status: parse_generation_status(&snapshot.generation_status), + }, + draft, + path: map_jump_hop_path(snapshot.path), + character_asset, + tile_atlas_asset, + tile_assets: snapshot + .tile_assets + .into_iter() + .map(map_tile_asset) + .collect(), + }) +} + +fn map_jump_hop_draft_snapshot(snapshot: JumpHopDraftSnapshot) -> JumpHopDraftResponse { + JumpHopDraftResponse { + template_id: snapshot.template_id, + template_name: snapshot.template_name, + profile_id: snapshot.profile_id, + work_title: snapshot.work_title, + work_description: snapshot.work_description, + theme_tags: snapshot.theme_tags, + difficulty: parse_difficulty(&snapshot.difficulty), + style_preset: parse_style_preset(&snapshot.style_preset), + character_prompt: snapshot.character_prompt, + tile_prompt: snapshot.tile_prompt, + end_mood_prompt: snapshot.end_mood_prompt, + character_asset: snapshot.character_asset.map(map_character_asset), + tile_atlas_asset: snapshot.tile_atlas_asset.map(map_character_asset), + tile_assets: snapshot + .tile_assets + .into_iter() + .map(map_tile_asset) + .collect(), + path: snapshot.path.map(map_jump_hop_path), + cover_composite: snapshot.cover_composite, + generation_status: parse_generation_status(&snapshot.generation_status), + } +} + +fn map_character_asset(snapshot: JumpHopCharacterAssetSnapshot) -> JumpHopCharacterAsset { + JumpHopCharacterAsset { + asset_id: snapshot.asset_id, + image_src: snapshot.image_src, + image_object_key: snapshot.image_object_key, + asset_object_id: snapshot.asset_object_id, + generation_provider: snapshot.generation_provider, + prompt: snapshot.prompt, + width: snapshot.width, + height: snapshot.height, + } +} + +fn map_tile_asset(snapshot: JumpHopTileAssetSnapshot) -> JumpHopTileAsset { + JumpHopTileAsset { + tile_type: parse_tile_type(&snapshot.tile_type), + image_src: snapshot.image_src, + image_object_key: snapshot.image_object_key, + asset_object_id: snapshot.asset_object_id, + source_atlas_cell: snapshot.source_atlas_cell, + visual_width: snapshot.visual_width, + visual_height: snapshot.visual_height, + top_surface_radius: snapshot.top_surface_radius, + landing_radius: snapshot.landing_radius, + } +} + +fn map_jump_hop_path(snapshot: crate::module_bindings::JumpHopPath) -> JumpHopPath { + JumpHopPath { + seed: snapshot.seed, + difficulty: parse_domain_difficulty(snapshot.difficulty), + platforms: snapshot + .platforms + .into_iter() + .map(|platform| JumpHopPlatform { + platform_id: platform.platform_id, + tile_type: parse_domain_tile_type(platform.tile_type), + x: platform.x, + y: platform.y, + width: platform.width, + height: platform.height, + landing_radius: platform.landing_radius, + perfect_radius: platform.perfect_radius, + score_value: platform.score_value, + }) + .collect(), + finish_index: snapshot.finish_index, + camera_preset: snapshot.camera_preset, + scoring: JumpHopScoring { + charge_to_distance_ratio: snapshot.scoring.charge_to_distance_ratio, + max_charge_ms: snapshot.scoring.max_charge_ms, + hit_bonus: snapshot.scoring.hit_bonus, + perfect_bonus: snapshot.scoring.perfect_bonus, + }, + } +} + +fn map_jump_hop_run_snapshot(snapshot: JumpHopRunSnapshot) -> JumpHopRuntimeRunSnapshotResponse { + JumpHopRuntimeRunSnapshotResponse { + run_id: snapshot.run_id, + profile_id: snapshot.profile_id, + owner_user_id: snapshot.owner_user_id, + status: match snapshot.status { + crate::module_bindings::JumpHopRunStatus::Failed => JumpHopRunStatus::Failed, + crate::module_bindings::JumpHopRunStatus::Cleared => JumpHopRunStatus::Cleared, + crate::module_bindings::JumpHopRunStatus::Playing => JumpHopRunStatus::Playing, + }, + current_platform_index: snapshot.current_platform_index, + score: snapshot.score, + combo: snapshot.combo, + path: map_jump_hop_path(snapshot.path), + last_jump: snapshot.last_jump.map(|jump| JumpHopLastJump { + charge_ms: jump.charge_ms, + jump_distance: jump.jump_distance, + target_platform_index: jump.target_platform_index, + landed_x: jump.landed_x, + landed_y: jump.landed_y, + result: match jump.result { + crate::module_bindings::JumpHopJumpResultKind::Miss => JumpHopJumpResult::Miss, + crate::module_bindings::JumpHopJumpResultKind::Hit => JumpHopJumpResult::Hit, + crate::module_bindings::JumpHopJumpResultKind::Finish => JumpHopJumpResult::Finish, + crate::module_bindings::JumpHopJumpResultKind::Perfect => { + JumpHopJumpResult::Perfect + } + }, + }), + started_at_ms: snapshot.started_at_ms, + finished_at_ms: snapshot.finished_at_ms, + } +} + +fn parse_difficulty(value: &str) -> JumpHopDifficulty { + match value { + "easy" => JumpHopDifficulty::Easy, + "advanced" => JumpHopDifficulty::Advanced, + "challenge" => JumpHopDifficulty::Challenge, + _ => JumpHopDifficulty::Standard, + } +} + +fn parse_domain_difficulty(value: crate::module_bindings::JumpHopDifficulty) -> JumpHopDifficulty { + match value { + crate::module_bindings::JumpHopDifficulty::Easy => JumpHopDifficulty::Easy, + crate::module_bindings::JumpHopDifficulty::Advanced => JumpHopDifficulty::Advanced, + crate::module_bindings::JumpHopDifficulty::Challenge => JumpHopDifficulty::Challenge, + crate::module_bindings::JumpHopDifficulty::Standard => JumpHopDifficulty::Standard, + } +} + +fn parse_style_preset(value: &str) -> JumpHopStylePreset { + match value { + "paper-toy" => JumpHopStylePreset::PaperToy, + "neon-glass" => JumpHopStylePreset::NeonGlass, + "forest-stone" => JumpHopStylePreset::ForestStone, + "future-metal" => JumpHopStylePreset::FutureMetal, + "custom" => JumpHopStylePreset::Custom, + _ => JumpHopStylePreset::MinimalBlocks, + } +} + +fn parse_tile_type(value: &str) -> JumpHopTileType { + match value { + "start" => JumpHopTileType::Start, + "target" => JumpHopTileType::Target, + "finish" => JumpHopTileType::Finish, + "bonus" => JumpHopTileType::Bonus, + "accent" => JumpHopTileType::Accent, + _ => JumpHopTileType::Normal, + } +} + +fn parse_domain_tile_type(value: crate::module_bindings::JumpHopTileType) -> JumpHopTileType { + match value { + crate::module_bindings::JumpHopTileType::Start => JumpHopTileType::Start, + crate::module_bindings::JumpHopTileType::Target => JumpHopTileType::Target, + crate::module_bindings::JumpHopTileType::Finish => JumpHopTileType::Finish, + crate::module_bindings::JumpHopTileType::Bonus => JumpHopTileType::Bonus, + crate::module_bindings::JumpHopTileType::Accent => JumpHopTileType::Accent, + crate::module_bindings::JumpHopTileType::Normal => JumpHopTileType::Normal, + } +} + +fn parse_generation_status(value: &str) -> JumpHopGenerationStatus { + match value { + "generating" => JumpHopGenerationStatus::Generating, + "ready" => JumpHopGenerationStatus::Ready, + "failed" => JumpHopGenerationStatus::Failed, + _ => JumpHopGenerationStatus::Draft, + } +} + +fn normalize_publication_status(value: &str) -> &str { + match value { + "Published" | "published" => "published", + _ => "draft", + } +} diff --git a/server-rs/crates/spacetime-client/src/mapper/puzzle.rs b/server-rs/crates/spacetime-client/src/mapper/puzzle.rs index ae67fd65..0b7d8ec6 100644 --- a/server-rs/crates/spacetime-client/src/mapper/puzzle.rs +++ b/server-rs/crates/spacetime-client/src/mapper/puzzle.rs @@ -145,6 +145,12 @@ pub(crate) fn map_puzzle_draft_level(snapshot: PuzzleDraftLevel) -> PuzzleDraftL ui_background_prompt: snapshot.ui_background_prompt, ui_background_image_src: snapshot.ui_background_image_src, ui_background_image_object_key: snapshot.ui_background_image_object_key, + level_scene_image_src: snapshot.level_scene_image_src, + level_scene_image_object_key: snapshot.level_scene_image_object_key, + ui_spritesheet_image_src: snapshot.ui_spritesheet_image_src, + ui_spritesheet_image_object_key: snapshot.ui_spritesheet_image_object_key, + level_background_image_src: snapshot.level_background_image_src, + level_background_image_object_key: snapshot.level_background_image_object_key, background_music: snapshot.background_music.map(map_puzzle_audio_asset), candidates: snapshot .candidates @@ -392,6 +398,10 @@ pub(crate) fn map_puzzle_runtime_level_snapshot( cover_image_src: snapshot.cover_image_src, ui_background_image_src: snapshot.ui_background_image_src, ui_background_image_object_key: snapshot.ui_background_image_object_key, + level_background_image_src: snapshot.level_background_image_src, + level_background_image_object_key: snapshot.level_background_image_object_key, + ui_spritesheet_image_src: snapshot.ui_spritesheet_image_src, + ui_spritesheet_image_object_key: snapshot.ui_spritesheet_image_object_key, background_music: snapshot.background_music.map(map_puzzle_audio_asset), board: map_puzzle_board_snapshot(snapshot.board), status: format_puzzle_runtime_level_status(snapshot.status).to_string(), @@ -835,6 +845,12 @@ pub struct PuzzleDraftLevelRecord { pub ui_background_prompt: Option, pub ui_background_image_src: Option, pub ui_background_image_object_key: Option, + pub level_scene_image_src: Option, + pub level_scene_image_object_key: Option, + pub ui_spritesheet_image_src: Option, + pub ui_spritesheet_image_object_key: Option, + pub level_background_image_src: Option, + pub level_background_image_object_key: Option, pub background_music: Option, pub candidates: Vec, pub selected_candidate_id: Option, @@ -1038,6 +1054,10 @@ pub struct PuzzleRuntimeLevelRecord { pub cover_image_src: Option, pub ui_background_image_src: Option, pub ui_background_image_object_key: Option, + pub level_background_image_src: Option, + pub level_background_image_object_key: Option, + pub ui_spritesheet_image_src: Option, + pub ui_spritesheet_image_object_key: Option, pub background_music: Option, pub board: PuzzleBoardRecord, pub status: String, diff --git a/server-rs/crates/spacetime-client/src/module_bindings.rs b/server-rs/crates/spacetime-client/src/module_bindings.rs index f6725232..c9a71a7f 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings.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.2.0 (commit eb11e2f5c41dce6979715ad407996270d61329f6). +// This was generated using spacetimedb cli version 2.1.0 (commit 6981f48b4bc1a71c8dd9bdfe5a2c343f6370243d). #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; @@ -201,6 +201,7 @@ pub mod click_match_3_d_item_procedure; pub mod combat_outcome_type; pub mod compile_big_fish_draft_procedure; pub mod compile_custom_world_published_profile_procedure; +pub mod compile_jump_hop_draft_procedure; pub mod compile_match_3_d_draft_procedure; pub mod compile_puzzle_agent_draft_procedure; pub mod compile_square_hole_draft_procedure; @@ -220,6 +221,7 @@ pub mod create_battle_state_and_return_procedure; pub mod create_battle_state_reducer; pub mod create_big_fish_session_procedure; pub mod create_custom_world_agent_session_procedure; +pub mod create_jump_hop_agent_session_procedure; pub mod create_match_3_d_agent_session_procedure; pub mod create_profile_recharge_order_and_return_procedure; pub mod create_puzzle_agent_session_procedure; @@ -351,6 +353,9 @@ pub mod get_custom_world_agent_session_procedure; pub mod get_custom_world_gallery_detail_by_code_procedure; pub mod get_custom_world_gallery_detail_procedure; pub mod get_custom_world_library_detail_procedure; +pub mod get_jump_hop_agent_session_procedure; +pub mod get_jump_hop_run_procedure; +pub mod get_jump_hop_work_profile_procedure; pub mod get_match_3_d_agent_session_procedure; pub mod get_match_3_d_run_procedure; pub mod get_match_3_d_work_detail_procedure; @@ -395,11 +400,55 @@ pub mod inventory_mutation_type; pub mod inventory_slot_snapshot_type; pub mod inventory_slot_table; pub mod inventory_slot_type; +pub mod jump_hop_agent_session_create_input_type; +pub mod jump_hop_agent_session_get_input_type; +pub mod jump_hop_agent_session_procedure_result_type; +pub mod jump_hop_agent_session_row_type; +pub mod jump_hop_agent_session_snapshot_type; +pub mod jump_hop_agent_session_table; +pub mod jump_hop_character_asset_snapshot_type; +pub mod jump_hop_creator_config_snapshot_type; +pub mod jump_hop_difficulty_type; +pub mod jump_hop_draft_compile_input_type; +pub mod jump_hop_draft_snapshot_type; +pub mod jump_hop_event_row_type; +pub mod jump_hop_event_table; +pub mod jump_hop_gallery_card_view_row_type; +pub mod jump_hop_gallery_card_view_table; +pub mod jump_hop_gallery_view_row_type; +pub mod jump_hop_gallery_view_table; +pub mod jump_hop_jump_procedure; +pub mod jump_hop_jump_result_kind_type; +pub mod jump_hop_last_jump_type; +pub mod jump_hop_path_type; +pub mod jump_hop_platform_type; +pub mod jump_hop_run_get_input_type; +pub mod jump_hop_run_jump_input_type; +pub mod jump_hop_run_procedure_result_type; +pub mod jump_hop_run_restart_input_type; +pub mod jump_hop_run_snapshot_type; +pub mod jump_hop_run_start_input_type; +pub mod jump_hop_run_status_type; +pub mod jump_hop_runtime_run_row_type; +pub mod jump_hop_runtime_run_table; +pub mod jump_hop_scoring_type; +pub mod jump_hop_tile_asset_snapshot_type; +pub mod jump_hop_tile_type_type; +pub mod jump_hop_work_get_input_type; +pub mod jump_hop_work_procedure_result_type; +pub mod jump_hop_work_profile_row_type; +pub mod jump_hop_work_profile_table; +pub mod jump_hop_work_publish_input_type; +pub mod jump_hop_work_snapshot_type; +pub mod jump_hop_work_update_input_type; +pub mod jump_hop_works_list_input_type; +pub mod jump_hop_works_procedure_result_type; pub mod list_asset_history_and_return_procedure; pub mod list_big_fish_works_procedure; pub mod list_custom_world_gallery_entries_procedure; pub mod list_custom_world_profiles_procedure; pub mod list_custom_world_works_procedure; +pub mod list_jump_hop_works_procedure; pub mod list_match_3_d_works_procedure; pub mod list_platform_browse_history_procedure; pub mod list_profile_save_archives_procedure; @@ -510,6 +559,7 @@ pub mod publish_big_fish_game_procedure; pub mod publish_custom_world_profile_and_return_procedure; pub mod publish_custom_world_profile_reducer; pub mod publish_custom_world_world_procedure; +pub mod publish_jump_hop_work_procedure; pub mod publish_match_3_d_work_procedure; pub mod publish_puzzle_work_procedure; pub mod publish_square_hole_work_procedure; @@ -651,6 +701,7 @@ pub mod resolve_npc_social_action_input_type; pub mod resolve_npc_social_action_reducer; pub mod resolve_treasure_interaction_and_return_procedure; pub mod resolve_treasure_interaction_reducer; +pub mod restart_jump_hop_run_procedure; pub mod restart_match_3_d_run_procedure; pub mod restart_square_hole_run_procedure; pub mod resume_profile_save_archive_and_return_procedure; @@ -822,6 +873,7 @@ 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_jump_hop_run_procedure; pub mod start_match_3_d_run_procedure; pub mod start_puzzle_run_procedure; pub mod start_square_hole_run_procedure; @@ -866,6 +918,7 @@ 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_jump_hop_work_procedure; pub mod update_match_3_d_work_procedure; pub mod update_puzzle_run_pause_procedure; pub mod update_puzzle_work_procedure; @@ -1129,6 +1182,7 @@ pub use click_match_3_d_item_procedure::click_match_3_d_item; pub use combat_outcome_type::CombatOutcome; pub use compile_big_fish_draft_procedure::compile_big_fish_draft; pub use compile_custom_world_published_profile_procedure::compile_custom_world_published_profile; +pub use compile_jump_hop_draft_procedure::compile_jump_hop_draft; pub use compile_match_3_d_draft_procedure::compile_match_3_d_draft; pub use compile_puzzle_agent_draft_procedure::compile_puzzle_agent_draft; pub use compile_square_hole_draft_procedure::compile_square_hole_draft; @@ -1148,6 +1202,7 @@ 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; pub use create_custom_world_agent_session_procedure::create_custom_world_agent_session; +pub use create_jump_hop_agent_session_procedure::create_jump_hop_agent_session; pub use create_match_3_d_agent_session_procedure::create_match_3_d_agent_session; pub use create_profile_recharge_order_and_return_procedure::create_profile_recharge_order_and_return; pub use create_puzzle_agent_session_procedure::create_puzzle_agent_session; @@ -1279,6 +1334,9 @@ pub use get_custom_world_agent_session_procedure::get_custom_world_agent_session pub use get_custom_world_gallery_detail_by_code_procedure::get_custom_world_gallery_detail_by_code; pub use get_custom_world_gallery_detail_procedure::get_custom_world_gallery_detail; pub use get_custom_world_library_detail_procedure::get_custom_world_library_detail; +pub use get_jump_hop_agent_session_procedure::get_jump_hop_agent_session; +pub use get_jump_hop_run_procedure::get_jump_hop_run; +pub use get_jump_hop_work_profile_procedure::get_jump_hop_work_profile; pub use get_match_3_d_agent_session_procedure::get_match_3_d_agent_session; pub use get_match_3_d_run_procedure::get_match_3_d_run; pub use get_match_3_d_work_detail_procedure::get_match_3_d_work_detail; @@ -1323,11 +1381,55 @@ pub use inventory_mutation_type::InventoryMutation; pub use inventory_slot_snapshot_type::InventorySlotSnapshot; pub use inventory_slot_table::*; pub use inventory_slot_type::InventorySlot; +pub use jump_hop_agent_session_create_input_type::JumpHopAgentSessionCreateInput; +pub use jump_hop_agent_session_get_input_type::JumpHopAgentSessionGetInput; +pub use jump_hop_agent_session_procedure_result_type::JumpHopAgentSessionProcedureResult; +pub use jump_hop_agent_session_row_type::JumpHopAgentSessionRow; +pub use jump_hop_agent_session_snapshot_type::JumpHopAgentSessionSnapshot; +pub use jump_hop_agent_session_table::*; +pub use jump_hop_character_asset_snapshot_type::JumpHopCharacterAssetSnapshot; +pub use jump_hop_creator_config_snapshot_type::JumpHopCreatorConfigSnapshot; +pub use jump_hop_difficulty_type::JumpHopDifficulty; +pub use jump_hop_draft_compile_input_type::JumpHopDraftCompileInput; +pub use jump_hop_draft_snapshot_type::JumpHopDraftSnapshot; +pub use jump_hop_event_row_type::JumpHopEventRow; +pub use jump_hop_event_table::*; +pub use jump_hop_gallery_card_view_row_type::JumpHopGalleryCardViewRow; +pub use jump_hop_gallery_card_view_table::*; +pub use jump_hop_gallery_view_row_type::JumpHopGalleryViewRow; +pub use jump_hop_gallery_view_table::*; +pub use jump_hop_jump_procedure::jump_hop_jump; +pub use jump_hop_jump_result_kind_type::JumpHopJumpResultKind; +pub use jump_hop_last_jump_type::JumpHopLastJump; +pub use jump_hop_path_type::JumpHopPath; +pub use jump_hop_platform_type::JumpHopPlatform; +pub use jump_hop_run_get_input_type::JumpHopRunGetInput; +pub use jump_hop_run_jump_input_type::JumpHopRunJumpInput; +pub use jump_hop_run_procedure_result_type::JumpHopRunProcedureResult; +pub use jump_hop_run_restart_input_type::JumpHopRunRestartInput; +pub use jump_hop_run_snapshot_type::JumpHopRunSnapshot; +pub use jump_hop_run_start_input_type::JumpHopRunStartInput; +pub use jump_hop_run_status_type::JumpHopRunStatus; +pub use jump_hop_runtime_run_row_type::JumpHopRuntimeRunRow; +pub use jump_hop_runtime_run_table::*; +pub use jump_hop_scoring_type::JumpHopScoring; +pub use jump_hop_tile_asset_snapshot_type::JumpHopTileAssetSnapshot; +pub use jump_hop_tile_type_type::JumpHopTileType; +pub use jump_hop_work_get_input_type::JumpHopWorkGetInput; +pub use jump_hop_work_procedure_result_type::JumpHopWorkProcedureResult; +pub use jump_hop_work_profile_row_type::JumpHopWorkProfileRow; +pub use jump_hop_work_profile_table::*; +pub use jump_hop_work_publish_input_type::JumpHopWorkPublishInput; +pub use jump_hop_work_snapshot_type::JumpHopWorkSnapshot; +pub use jump_hop_work_update_input_type::JumpHopWorkUpdateInput; +pub use jump_hop_works_list_input_type::JumpHopWorksListInput; +pub use jump_hop_works_procedure_result_type::JumpHopWorksProcedureResult; pub use list_asset_history_and_return_procedure::list_asset_history_and_return; pub use list_big_fish_works_procedure::list_big_fish_works; pub use list_custom_world_gallery_entries_procedure::list_custom_world_gallery_entries; pub use list_custom_world_profiles_procedure::list_custom_world_profiles; pub use list_custom_world_works_procedure::list_custom_world_works; +pub use list_jump_hop_works_procedure::list_jump_hop_works; pub use list_match_3_d_works_procedure::list_match_3_d_works; pub use list_platform_browse_history_procedure::list_platform_browse_history; pub use list_profile_save_archives_procedure::list_profile_save_archives; @@ -1438,6 +1540,7 @@ 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; pub use publish_custom_world_world_procedure::publish_custom_world_world; +pub use publish_jump_hop_work_procedure::publish_jump_hop_work; pub use publish_match_3_d_work_procedure::publish_match_3_d_work; pub use publish_puzzle_work_procedure::publish_puzzle_work; pub use publish_square_hole_work_procedure::publish_square_hole_work; @@ -1579,6 +1682,7 @@ pub use resolve_npc_social_action_input_type::ResolveNpcSocialActionInput; pub use resolve_npc_social_action_reducer::resolve_npc_social_action; pub use resolve_treasure_interaction_and_return_procedure::resolve_treasure_interaction_and_return; pub use resolve_treasure_interaction_reducer::resolve_treasure_interaction; +pub use restart_jump_hop_run_procedure::restart_jump_hop_run; pub use restart_match_3_d_run_procedure::restart_match_3_d_run; pub use restart_square_hole_run_procedure::restart_square_hole_run; pub use resume_profile_save_archive_and_return_procedure::resume_profile_save_archive_and_return; @@ -1750,6 +1854,7 @@ 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_jump_hop_run_procedure::start_jump_hop_run; pub use start_match_3_d_run_procedure::start_match_3_d_run; pub use start_puzzle_run_procedure::start_puzzle_run; pub use start_square_hole_run_procedure::start_square_hole_run; @@ -1794,6 +1899,7 @@ 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_jump_hop_work_procedure::update_jump_hop_work; 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; @@ -2174,6 +2280,12 @@ pub struct DbUpdate { database_migration_import_chunk: __sdk::TableUpdate, database_migration_operator: __sdk::TableUpdate, inventory_slot: __sdk::TableUpdate, + jump_hop_agent_session: __sdk::TableUpdate, + jump_hop_event: __sdk::TableUpdate, + jump_hop_gallery_card_view: __sdk::TableUpdate, + jump_hop_gallery_view: __sdk::TableUpdate, + jump_hop_runtime_run: __sdk::TableUpdate, + jump_hop_work_profile: __sdk::TableUpdate, match_3_d_agent_message: __sdk::TableUpdate, match_3_d_agent_session: __sdk::TableUpdate, match_3_d_gallery_view: __sdk::TableUpdate, @@ -2366,6 +2478,24 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { "inventory_slot" => db_update .inventory_slot .append(inventory_slot_table::parse_table_update(table_update)?), + "jump_hop_agent_session" => db_update.jump_hop_agent_session.append( + jump_hop_agent_session_table::parse_table_update(table_update)?, + ), + "jump_hop_event" => db_update + .jump_hop_event + .append(jump_hop_event_table::parse_table_update(table_update)?), + "jump_hop_gallery_card_view" => db_update.jump_hop_gallery_card_view.append( + jump_hop_gallery_card_view_table::parse_table_update(table_update)?, + ), + "jump_hop_gallery_view" => db_update.jump_hop_gallery_view.append( + jump_hop_gallery_view_table::parse_table_update(table_update)?, + ), + "jump_hop_runtime_run" => db_update.jump_hop_runtime_run.append( + jump_hop_runtime_run_table::parse_table_update(table_update)?, + ), + "jump_hop_work_profile" => db_update.jump_hop_work_profile.append( + jump_hop_work_profile_table::parse_table_update(table_update)?, + ), "match_3_d_agent_message" => db_update.match_3_d_agent_message.append( match_3_d_agent_message_table::parse_table_update(table_update)?, ), @@ -2756,6 +2886,27 @@ impl __sdk::DbUpdate for DbUpdate { diff.inventory_slot = cache .apply_diff_to_table::("inventory_slot", &self.inventory_slot) .with_updates_by_pk(|row| &row.slot_id); + diff.jump_hop_agent_session = cache + .apply_diff_to_table::( + "jump_hop_agent_session", + &self.jump_hop_agent_session, + ) + .with_updates_by_pk(|row| &row.session_id); + diff.jump_hop_event = cache + .apply_diff_to_table::("jump_hop_event", &self.jump_hop_event) + .with_updates_by_pk(|row| &row.event_id); + diff.jump_hop_runtime_run = cache + .apply_diff_to_table::( + "jump_hop_runtime_run", + &self.jump_hop_runtime_run, + ) + .with_updates_by_pk(|row| &row.run_id); + diff.jump_hop_work_profile = cache + .apply_diff_to_table::( + "jump_hop_work_profile", + &self.jump_hop_work_profile, + ) + .with_updates_by_pk(|row| &row.profile_id); diff.match_3_d_agent_message = cache .apply_diff_to_table::( "match_3_d_agent_message", @@ -3024,6 +3175,14 @@ impl __sdk::DbUpdate for DbUpdate { "big_fish_gallery_view", &self.big_fish_gallery_view, ); + diff.jump_hop_gallery_card_view = cache.apply_diff_to_table::( + "jump_hop_gallery_card_view", + &self.jump_hop_gallery_card_view, + ); + diff.jump_hop_gallery_view = cache.apply_diff_to_table::( + "jump_hop_gallery_view", + &self.jump_hop_gallery_view, + ); diff.match_3_d_gallery_view = cache.apply_diff_to_table::( "match_3_d_gallery_view", &self.match_3_d_gallery_view, @@ -3171,6 +3330,24 @@ impl __sdk::DbUpdate for DbUpdate { "inventory_slot" => db_update .inventory_slot .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "jump_hop_agent_session" => db_update + .jump_hop_agent_session + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "jump_hop_event" => db_update + .jump_hop_event + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "jump_hop_gallery_card_view" => db_update + .jump_hop_gallery_card_view + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "jump_hop_gallery_view" => db_update + .jump_hop_gallery_view + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "jump_hop_runtime_run" => db_update + .jump_hop_runtime_run + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "jump_hop_work_profile" => db_update + .jump_hop_work_profile + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "match_3_d_agent_message" => db_update .match_3_d_agent_message .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), @@ -3472,6 +3649,24 @@ impl __sdk::DbUpdate for DbUpdate { "inventory_slot" => db_update .inventory_slot .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "jump_hop_agent_session" => db_update + .jump_hop_agent_session + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "jump_hop_event" => db_update + .jump_hop_event + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "jump_hop_gallery_card_view" => db_update + .jump_hop_gallery_card_view + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "jump_hop_gallery_view" => db_update + .jump_hop_gallery_view + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "jump_hop_runtime_run" => db_update + .jump_hop_runtime_run + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "jump_hop_work_profile" => db_update + .jump_hop_work_profile + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "match_3_d_agent_message" => db_update .match_3_d_agent_message .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), @@ -3697,6 +3892,12 @@ pub struct AppliedDiff<'r> { database_migration_import_chunk: __sdk::TableAppliedDiff<'r, DatabaseMigrationImportChunk>, database_migration_operator: __sdk::TableAppliedDiff<'r, DatabaseMigrationOperator>, inventory_slot: __sdk::TableAppliedDiff<'r, InventorySlot>, + jump_hop_agent_session: __sdk::TableAppliedDiff<'r, JumpHopAgentSessionRow>, + jump_hop_event: __sdk::TableAppliedDiff<'r, JumpHopEventRow>, + jump_hop_gallery_card_view: __sdk::TableAppliedDiff<'r, JumpHopGalleryCardViewRow>, + jump_hop_gallery_view: __sdk::TableAppliedDiff<'r, JumpHopGalleryViewRow>, + jump_hop_runtime_run: __sdk::TableAppliedDiff<'r, JumpHopRuntimeRunRow>, + jump_hop_work_profile: __sdk::TableAppliedDiff<'r, JumpHopWorkProfileRow>, match_3_d_agent_message: __sdk::TableAppliedDiff<'r, Match3DAgentMessageRow>, match_3_d_agent_session: __sdk::TableAppliedDiff<'r, Match3DAgentSessionRow>, match_3_d_gallery_view: __sdk::TableAppliedDiff<'r, Match3DGalleryViewRow>, @@ -3959,6 +4160,36 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { &self.inventory_slot, event, ); + callbacks.invoke_table_row_callbacks::( + "jump_hop_agent_session", + &self.jump_hop_agent_session, + event, + ); + callbacks.invoke_table_row_callbacks::( + "jump_hop_event", + &self.jump_hop_event, + event, + ); + callbacks.invoke_table_row_callbacks::( + "jump_hop_gallery_card_view", + &self.jump_hop_gallery_card_view, + event, + ); + callbacks.invoke_table_row_callbacks::( + "jump_hop_gallery_view", + &self.jump_hop_gallery_view, + event, + ); + callbacks.invoke_table_row_callbacks::( + "jump_hop_runtime_run", + &self.jump_hop_runtime_run, + event, + ); + callbacks.invoke_table_row_callbacks::( + "jump_hop_work_profile", + &self.jump_hop_work_profile, + event, + ); callbacks.invoke_table_row_callbacks::( "match_3_d_agent_message", &self.match_3_d_agent_message, @@ -4927,6 +5158,12 @@ impl __sdk::SpacetimeModule for RemoteModule { database_migration_import_chunk_table::register_table(client_cache); database_migration_operator_table::register_table(client_cache); inventory_slot_table::register_table(client_cache); + jump_hop_agent_session_table::register_table(client_cache); + jump_hop_event_table::register_table(client_cache); + jump_hop_gallery_card_view_table::register_table(client_cache); + jump_hop_gallery_view_table::register_table(client_cache); + jump_hop_runtime_run_table::register_table(client_cache); + jump_hop_work_profile_table::register_table(client_cache); match_3_d_agent_message_table::register_table(client_cache); match_3_d_agent_session_table::register_table(client_cache); match_3_d_gallery_view_table::register_table(client_cache); @@ -5025,6 +5262,12 @@ impl __sdk::SpacetimeModule for RemoteModule { "database_migration_import_chunk", "database_migration_operator", "inventory_slot", + "jump_hop_agent_session", + "jump_hop_event", + "jump_hop_gallery_card_view", + "jump_hop_gallery_view", + "jump_hop_runtime_run", + "jump_hop_work_profile", "match_3_d_agent_message", "match_3_d_agent_session", "match_3_d_gallery_view", diff --git a/server-rs/crates/spacetime-client/src/module_bindings/compile_jump_hop_draft_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/compile_jump_hop_draft_procedure.rs new file mode 100644 index 00000000..f0479afa --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/compile_jump_hop_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::jump_hop_agent_session_procedure_result_type::JumpHopAgentSessionProcedureResult; +use super::jump_hop_draft_compile_input_type::JumpHopDraftCompileInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct CompileJumpHopDraftArgs { + pub input: JumpHopDraftCompileInput, +} + +impl __sdk::InModule for CompileJumpHopDraftArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `compile_jump_hop_draft`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait compile_jump_hop_draft { + fn compile_jump_hop_draft(&self, input: JumpHopDraftCompileInput) { + self.compile_jump_hop_draft_then(input, |_, _| {}); + } + + fn compile_jump_hop_draft_then( + &self, + input: JumpHopDraftCompileInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl compile_jump_hop_draft for super::RemoteProcedures { + fn compile_jump_hop_draft_then( + &self, + input: JumpHopDraftCompileInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, JumpHopAgentSessionProcedureResult>( + "compile_jump_hop_draft", + CompileJumpHopDraftArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/create_jump_hop_agent_session_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/create_jump_hop_agent_session_procedure.rs new file mode 100644 index 00000000..6bfce7c5 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/create_jump_hop_agent_session_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::jump_hop_agent_session_create_input_type::JumpHopAgentSessionCreateInput; +use super::jump_hop_agent_session_procedure_result_type::JumpHopAgentSessionProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct CreateJumpHopAgentSessionArgs { + pub input: JumpHopAgentSessionCreateInput, +} + +impl __sdk::InModule for CreateJumpHopAgentSessionArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `create_jump_hop_agent_session`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait create_jump_hop_agent_session { + fn create_jump_hop_agent_session(&self, input: JumpHopAgentSessionCreateInput) { + self.create_jump_hop_agent_session_then(input, |_, _| {}); + } + + fn create_jump_hop_agent_session_then( + &self, + input: JumpHopAgentSessionCreateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl create_jump_hop_agent_session for super::RemoteProcedures { + fn create_jump_hop_agent_session_then( + &self, + input: JumpHopAgentSessionCreateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, JumpHopAgentSessionProcedureResult>( + "create_jump_hop_agent_session", + CreateJumpHopAgentSessionArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_jump_hop_agent_session_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_jump_hop_agent_session_procedure.rs new file mode 100644 index 00000000..482aa1a5 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_jump_hop_agent_session_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::jump_hop_agent_session_get_input_type::JumpHopAgentSessionGetInput; +use super::jump_hop_agent_session_procedure_result_type::JumpHopAgentSessionProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct GetJumpHopAgentSessionArgs { + pub input: JumpHopAgentSessionGetInput, +} + +impl __sdk::InModule for GetJumpHopAgentSessionArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_jump_hop_agent_session`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_jump_hop_agent_session { + fn get_jump_hop_agent_session(&self, input: JumpHopAgentSessionGetInput) { + self.get_jump_hop_agent_session_then(input, |_, _| {}); + } + + fn get_jump_hop_agent_session_then( + &self, + input: JumpHopAgentSessionGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl get_jump_hop_agent_session for super::RemoteProcedures { + fn get_jump_hop_agent_session_then( + &self, + input: JumpHopAgentSessionGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, JumpHopAgentSessionProcedureResult>( + "get_jump_hop_agent_session", + GetJumpHopAgentSessionArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_jump_hop_run_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_jump_hop_run_procedure.rs new file mode 100644 index 00000000..5c301da7 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_jump_hop_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::jump_hop_run_get_input_type::JumpHopRunGetInput; +use super::jump_hop_run_procedure_result_type::JumpHopRunProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct GetJumpHopRunArgs { + pub input: JumpHopRunGetInput, +} + +impl __sdk::InModule for GetJumpHopRunArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_jump_hop_run`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_jump_hop_run { + fn get_jump_hop_run(&self, input: JumpHopRunGetInput) { + self.get_jump_hop_run_then(input, |_, _| {}); + } + + fn get_jump_hop_run_then( + &self, + input: JumpHopRunGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl get_jump_hop_run for super::RemoteProcedures { + fn get_jump_hop_run_then( + &self, + input: JumpHopRunGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, JumpHopRunProcedureResult>( + "get_jump_hop_run", + GetJumpHopRunArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_jump_hop_work_profile_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_jump_hop_work_profile_procedure.rs new file mode 100644 index 00000000..fd1fbd3e --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_jump_hop_work_profile_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::jump_hop_work_get_input_type::JumpHopWorkGetInput; +use super::jump_hop_work_procedure_result_type::JumpHopWorkProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct GetJumpHopWorkProfileArgs { + pub input: JumpHopWorkGetInput, +} + +impl __sdk::InModule for GetJumpHopWorkProfileArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_jump_hop_work_profile`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_jump_hop_work_profile { + fn get_jump_hop_work_profile(&self, input: JumpHopWorkGetInput) { + self.get_jump_hop_work_profile_then(input, |_, _| {}); + } + + fn get_jump_hop_work_profile_then( + &self, + input: JumpHopWorkGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl get_jump_hop_work_profile for super::RemoteProcedures { + fn get_jump_hop_work_profile_then( + &self, + input: JumpHopWorkGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, JumpHopWorkProcedureResult>( + "get_jump_hop_work_profile", + GetJumpHopWorkProfileArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_agent_session_create_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_agent_session_create_input_type.rs new file mode 100644 index 00000000..14e2f410 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_agent_session_create_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 JumpHopAgentSessionCreateInput { + pub session_id: String, + pub owner_user_id: String, + pub seed_text: String, + pub work_title: String, + pub work_description: String, + pub theme_tags_json: Option, + pub welcome_message_text: String, + pub config_json: Option, + pub created_at_micros: i64, +} + +impl __sdk::InModule for JumpHopAgentSessionCreateInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_agent_session_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_agent_session_get_input_type.rs new file mode 100644 index 00000000..cf6b9b5d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_agent_session_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 JumpHopAgentSessionGetInput { + pub session_id: String, + pub owner_user_id: String, +} + +impl __sdk::InModule for JumpHopAgentSessionGetInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_agent_session_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_agent_session_procedure_result_type.rs new file mode 100644 index 00000000..1d833be0 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_agent_session_procedure_result_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}; + +use super::jump_hop_agent_session_snapshot_type::JumpHopAgentSessionSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct JumpHopAgentSessionProcedureResult { + pub ok: bool, + pub session: Option, + pub error_message: Option, +} + +impl __sdk::InModule for JumpHopAgentSessionProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_agent_session_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_agent_session_row_type.rs new file mode 100644 index 00000000..9783325f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_agent_session_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 JumpHopAgentSessionRow { + pub session_id: String, + pub owner_user_id: String, + pub seed_text: String, + pub current_turn: u32, + pub progress_percent: u32, + pub stage: String, + pub config_json: String, + pub draft_json: String, + pub last_assistant_reply: String, + pub published_profile_id: String, + pub created_at: __sdk::Timestamp, + pub updated_at: __sdk::Timestamp, +} + +impl __sdk::InModule for JumpHopAgentSessionRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `JumpHopAgentSessionRow`. +/// +/// Provides typed access to columns for query building. +pub struct JumpHopAgentSessionRowCols { + pub session_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub seed_text: __sdk::__query_builder::Col, + pub current_turn: __sdk::__query_builder::Col, + pub progress_percent: __sdk::__query_builder::Col, + pub stage: __sdk::__query_builder::Col, + pub config_json: __sdk::__query_builder::Col, + pub draft_json: __sdk::__query_builder::Col, + pub last_assistant_reply: __sdk::__query_builder::Col, + pub published_profile_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 JumpHopAgentSessionRow { + type Cols = JumpHopAgentSessionRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + JumpHopAgentSessionRowCols { + session_id: __sdk::__query_builder::Col::new(table_name, "session_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + seed_text: __sdk::__query_builder::Col::new(table_name, "seed_text"), + current_turn: __sdk::__query_builder::Col::new(table_name, "current_turn"), + progress_percent: __sdk::__query_builder::Col::new(table_name, "progress_percent"), + stage: __sdk::__query_builder::Col::new(table_name, "stage"), + config_json: __sdk::__query_builder::Col::new(table_name, "config_json"), + draft_json: __sdk::__query_builder::Col::new(table_name, "draft_json"), + last_assistant_reply: __sdk::__query_builder::Col::new( + table_name, + "last_assistant_reply", + ), + published_profile_id: __sdk::__query_builder::Col::new( + table_name, + "published_profile_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 `JumpHopAgentSessionRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct JumpHopAgentSessionRowIxCols { + pub owner_user_id: __sdk::__query_builder::IxCol, + pub session_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for JumpHopAgentSessionRow { + type IxCols = JumpHopAgentSessionRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + JumpHopAgentSessionRowIxCols { + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + session_id: __sdk::__query_builder::IxCol::new(table_name, "session_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for JumpHopAgentSessionRow {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_agent_session_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_agent_session_snapshot_type.rs new file mode 100644 index 00000000..aaaacacc --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_agent_session_snapshot_type.rs @@ -0,0 +1,29 @@ +// 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::jump_hop_creator_config_snapshot_type::JumpHopCreatorConfigSnapshot; +use super::jump_hop_draft_snapshot_type::JumpHopDraftSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct JumpHopAgentSessionSnapshot { + pub session_id: String, + pub owner_user_id: String, + pub seed_text: String, + pub current_turn: u32, + pub progress_percent: u32, + pub stage: String, + pub config: JumpHopCreatorConfigSnapshot, + pub draft: Option, + pub last_assistant_reply: String, + pub published_profile_id: Option, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for JumpHopAgentSessionSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_agent_session_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_agent_session_table.rs new file mode 100644 index 00000000..7e77ef24 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_agent_session_table.rs @@ -0,0 +1,161 @@ +// 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::jump_hop_agent_session_row_type::JumpHopAgentSessionRow; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `jump_hop_agent_session`. +/// +/// Obtain a handle from the [`JumpHopAgentSessionTableAccess::jump_hop_agent_session`] method on [`super::RemoteTables`], +/// like `ctx.db.jump_hop_agent_session()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.jump_hop_agent_session().on_insert(...)`. +pub struct JumpHopAgentSessionTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `jump_hop_agent_session`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait JumpHopAgentSessionTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`JumpHopAgentSessionTableHandle`], which mediates access to the table `jump_hop_agent_session`. + fn jump_hop_agent_session(&self) -> JumpHopAgentSessionTableHandle<'_>; +} + +impl JumpHopAgentSessionTableAccess for super::RemoteTables { + fn jump_hop_agent_session(&self) -> JumpHopAgentSessionTableHandle<'_> { + JumpHopAgentSessionTableHandle { + imp: self + .imp + .get_table::("jump_hop_agent_session"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct JumpHopAgentSessionInsertCallbackId(__sdk::CallbackId); +pub struct JumpHopAgentSessionDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for JumpHopAgentSessionTableHandle<'ctx> { + type Row = JumpHopAgentSessionRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = JumpHopAgentSessionInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> JumpHopAgentSessionInsertCallbackId { + JumpHopAgentSessionInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: JumpHopAgentSessionInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = JumpHopAgentSessionDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> JumpHopAgentSessionDeleteCallbackId { + JumpHopAgentSessionDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: JumpHopAgentSessionDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct JumpHopAgentSessionUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for JumpHopAgentSessionTableHandle<'ctx> { + type UpdateCallbackId = JumpHopAgentSessionUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> JumpHopAgentSessionUpdateCallbackId { + JumpHopAgentSessionUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: JumpHopAgentSessionUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `session_id` unique index on the table `jump_hop_agent_session`, +/// which allows point queries on the field of the same name +/// via the [`JumpHopAgentSessionSessionIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.jump_hop_agent_session().session_id().find(...)`. +pub struct JumpHopAgentSessionSessionIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> JumpHopAgentSessionTableHandle<'ctx> { + /// Get a handle on the `session_id` unique index on the table `jump_hop_agent_session`. + pub fn session_id(&self) -> JumpHopAgentSessionSessionIdUnique<'ctx> { + JumpHopAgentSessionSessionIdUnique { + imp: self.imp.get_unique_constraint::("session_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> JumpHopAgentSessionSessionIdUnique<'ctx> { + /// Find the subscribed row whose `session_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::("jump_hop_agent_session"); + _table.add_unique_constraint::("session_id", |row| &row.session_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 `JumpHopAgentSessionRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait jump_hop_agent_sessionQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `JumpHopAgentSessionRow`. + fn jump_hop_agent_session(&self) -> __sdk::__query_builder::Table; +} + +impl jump_hop_agent_sessionQueryTableAccess for __sdk::QueryTableAccessor { + fn jump_hop_agent_session(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("jump_hop_agent_session") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_character_asset_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_character_asset_snapshot_type.rs new file mode 100644 index 00000000..e562198f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_character_asset_snapshot_type.rs @@ -0,0 +1,22 @@ +// 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 JumpHopCharacterAssetSnapshot { + pub asset_id: String, + pub image_src: String, + pub image_object_key: String, + pub asset_object_id: String, + pub generation_provider: String, + pub prompt: String, + pub width: u32, + pub height: u32, +} + +impl __sdk::InModule for JumpHopCharacterAssetSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_creator_config_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_creator_config_snapshot_type.rs new file mode 100644 index 00000000..6d0f3738 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_creator_config_snapshot_type.rs @@ -0,0 +1,20 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct JumpHopCreatorConfigSnapshot { + pub theme_text: String, + pub difficulty: String, + pub style_preset: String, + pub character_prompt: String, + pub tile_prompt: String, + pub end_mood_prompt: String, +} + +impl __sdk::InModule for JumpHopCreatorConfigSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_difficulty_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_difficulty_type.rs new file mode 100644 index 00000000..4e89783c --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_difficulty_type.rs @@ -0,0 +1,22 @@ +// 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)] +#[derive(Copy, Eq, Hash)] +pub enum JumpHopDifficulty { + Easy, + + Standard, + + Advanced, + + Challenge, +} + +impl __sdk::InModule for JumpHopDifficulty { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_draft_compile_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_draft_compile_input_type.rs new file mode 100644 index 00000000..d8f3e7f5 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_draft_compile_input_type.rs @@ -0,0 +1,34 @@ +// 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 JumpHopDraftCompileInput { + pub session_id: String, + pub owner_user_id: String, + pub profile_id: String, + pub author_display_name: String, + pub seed_text: String, + pub work_title: String, + pub work_description: String, + pub theme_tags_json: Option, + pub theme_text: Option, + pub difficulty: Option, + pub style_preset: Option, + pub character_prompt: Option, + pub tile_prompt: Option, + pub end_mood_prompt: Option, + pub character_asset_json: Option, + pub tile_atlas_asset_json: Option, + pub tile_assets_json: Option, + pub cover_composite: Option, + pub generation_status: Option, + pub compiled_at_micros: i64, +} + +impl __sdk::InModule for JumpHopDraftCompileInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_draft_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_draft_snapshot_type.rs new file mode 100644 index 00000000..09e12197 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_draft_snapshot_type.rs @@ -0,0 +1,35 @@ +// 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::jump_hop_character_asset_snapshot_type::JumpHopCharacterAssetSnapshot; +use super::jump_hop_path_type::JumpHopPath; +use super::jump_hop_tile_asset_snapshot_type::JumpHopTileAssetSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct JumpHopDraftSnapshot { + pub template_id: String, + pub template_name: String, + pub profile_id: Option, + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + pub difficulty: String, + pub style_preset: String, + pub character_prompt: String, + pub tile_prompt: String, + pub end_mood_prompt: Option, + pub character_asset: Option, + pub tile_atlas_asset: Option, + pub tile_assets: Vec, + pub path: Option, + pub cover_composite: Option, + pub generation_status: String, +} + +impl __sdk::InModule for JumpHopDraftSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_event_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_event_row_type.rs new file mode 100644 index 00000000..6e0fe7e2 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_event_row_type.rs @@ -0,0 +1,71 @@ +// 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 JumpHopEventRow { + pub event_id: String, + pub owner_user_id: String, + pub profile_id: String, + pub run_id: String, + pub event_type: String, + pub result: String, + pub occurred_at: __sdk::Timestamp, +} + +impl __sdk::InModule for JumpHopEventRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `JumpHopEventRow`. +/// +/// Provides typed access to columns for query building. +pub struct JumpHopEventRowCols { + pub event_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub profile_id: __sdk::__query_builder::Col, + pub run_id: __sdk::__query_builder::Col, + pub event_type: __sdk::__query_builder::Col, + pub result: __sdk::__query_builder::Col, + pub occurred_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for JumpHopEventRow { + type Cols = JumpHopEventRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + JumpHopEventRowCols { + event_id: __sdk::__query_builder::Col::new(table_name, "event_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"), + run_id: __sdk::__query_builder::Col::new(table_name, "run_id"), + event_type: __sdk::__query_builder::Col::new(table_name, "event_type"), + result: __sdk::__query_builder::Col::new(table_name, "result"), + occurred_at: __sdk::__query_builder::Col::new(table_name, "occurred_at"), + } + } +} + +/// Indexed column accessor struct for the table `JumpHopEventRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct JumpHopEventRowIxCols { + pub event_id: __sdk::__query_builder::IxCol, + pub profile_id: __sdk::__query_builder::IxCol, + pub run_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for JumpHopEventRow { + type IxCols = JumpHopEventRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + JumpHopEventRowIxCols { + event_id: __sdk::__query_builder::IxCol::new(table_name, "event_id"), + profile_id: __sdk::__query_builder::IxCol::new(table_name, "profile_id"), + run_id: __sdk::__query_builder::IxCol::new(table_name, "run_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for JumpHopEventRow {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_event_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_event_table.rs new file mode 100644 index 00000000..8070f7a8 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_event_table.rs @@ -0,0 +1,159 @@ +// 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::jump_hop_event_row_type::JumpHopEventRow; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `jump_hop_event`. +/// +/// Obtain a handle from the [`JumpHopEventTableAccess::jump_hop_event`] method on [`super::RemoteTables`], +/// like `ctx.db.jump_hop_event()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.jump_hop_event().on_insert(...)`. +pub struct JumpHopEventTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `jump_hop_event`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait JumpHopEventTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`JumpHopEventTableHandle`], which mediates access to the table `jump_hop_event`. + fn jump_hop_event(&self) -> JumpHopEventTableHandle<'_>; +} + +impl JumpHopEventTableAccess for super::RemoteTables { + fn jump_hop_event(&self) -> JumpHopEventTableHandle<'_> { + JumpHopEventTableHandle { + imp: self.imp.get_table::("jump_hop_event"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct JumpHopEventInsertCallbackId(__sdk::CallbackId); +pub struct JumpHopEventDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for JumpHopEventTableHandle<'ctx> { + type Row = JumpHopEventRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = JumpHopEventInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> JumpHopEventInsertCallbackId { + JumpHopEventInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: JumpHopEventInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = JumpHopEventDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> JumpHopEventDeleteCallbackId { + JumpHopEventDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: JumpHopEventDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct JumpHopEventUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for JumpHopEventTableHandle<'ctx> { + type UpdateCallbackId = JumpHopEventUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> JumpHopEventUpdateCallbackId { + JumpHopEventUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: JumpHopEventUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `event_id` unique index on the table `jump_hop_event`, +/// which allows point queries on the field of the same name +/// via the [`JumpHopEventEventIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.jump_hop_event().event_id().find(...)`. +pub struct JumpHopEventEventIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> JumpHopEventTableHandle<'ctx> { + /// Get a handle on the `event_id` unique index on the table `jump_hop_event`. + pub fn event_id(&self) -> JumpHopEventEventIdUnique<'ctx> { + JumpHopEventEventIdUnique { + imp: self.imp.get_unique_constraint::("event_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> JumpHopEventEventIdUnique<'ctx> { + /// Find the subscribed row whose `event_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::("jump_hop_event"); + _table.add_unique_constraint::("event_id", |row| &row.event_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 `JumpHopEventRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait jump_hop_eventQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `JumpHopEventRow`. + fn jump_hop_event(&self) -> __sdk::__query_builder::Table; +} + +impl jump_hop_eventQueryTableAccess for __sdk::QueryTableAccessor { + fn jump_hop_event(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("jump_hop_event") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_gallery_card_view_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_gallery_card_view_row_type.rs new file mode 100644 index 00000000..25622a80 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_gallery_card_view_row_type.rs @@ -0,0 +1,82 @@ +// 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 JumpHopGalleryCardViewRow { + pub public_work_code: String, + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + pub difficulty: String, + pub style_preset: String, + pub cover_image_src: String, + pub publication_status: String, + pub play_count: u32, + pub updated_at_micros: i64, + pub published_at_micros: Option, + pub generation_status: String, +} + +impl __sdk::InModule for JumpHopGalleryCardViewRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `JumpHopGalleryCardViewRow`. +/// +/// Provides typed access to columns for query building. +pub struct JumpHopGalleryCardViewRowCols { + pub public_work_code: __sdk::__query_builder::Col, + pub work_id: __sdk::__query_builder::Col, + pub profile_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub author_display_name: __sdk::__query_builder::Col, + pub work_title: __sdk::__query_builder::Col, + pub work_description: __sdk::__query_builder::Col, + pub theme_tags: __sdk::__query_builder::Col>, + pub difficulty: __sdk::__query_builder::Col, + pub style_preset: __sdk::__query_builder::Col, + pub cover_image_src: __sdk::__query_builder::Col, + pub publication_status: __sdk::__query_builder::Col, + pub play_count: __sdk::__query_builder::Col, + pub updated_at_micros: __sdk::__query_builder::Col, + pub published_at_micros: __sdk::__query_builder::Col>, + pub generation_status: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for JumpHopGalleryCardViewRow { + type Cols = JumpHopGalleryCardViewRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + JumpHopGalleryCardViewRowCols { + public_work_code: __sdk::__query_builder::Col::new(table_name, "public_work_code"), + work_id: __sdk::__query_builder::Col::new(table_name, "work_id"), + profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + author_display_name: __sdk::__query_builder::Col::new( + table_name, + "author_display_name", + ), + work_title: __sdk::__query_builder::Col::new(table_name, "work_title"), + work_description: __sdk::__query_builder::Col::new(table_name, "work_description"), + theme_tags: __sdk::__query_builder::Col::new(table_name, "theme_tags"), + difficulty: __sdk::__query_builder::Col::new(table_name, "difficulty"), + style_preset: __sdk::__query_builder::Col::new(table_name, "style_preset"), + cover_image_src: __sdk::__query_builder::Col::new(table_name, "cover_image_src"), + publication_status: __sdk::__query_builder::Col::new(table_name, "publication_status"), + play_count: __sdk::__query_builder::Col::new(table_name, "play_count"), + updated_at_micros: __sdk::__query_builder::Col::new(table_name, "updated_at_micros"), + published_at_micros: __sdk::__query_builder::Col::new( + table_name, + "published_at_micros", + ), + generation_status: __sdk::__query_builder::Col::new(table_name, "generation_status"), + } + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_gallery_card_view_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_gallery_card_view_table.rs new file mode 100644 index 00000000..719ad477 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_gallery_card_view_table.rs @@ -0,0 +1,118 @@ +// 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::jump_hop_gallery_card_view_row_type::JumpHopGalleryCardViewRow; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `jump_hop_gallery_card_view`. +/// +/// Obtain a handle from the [`JumpHopGalleryCardViewTableAccess::jump_hop_gallery_card_view`] method on [`super::RemoteTables`], +/// like `ctx.db.jump_hop_gallery_card_view()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.jump_hop_gallery_card_view().on_insert(...)`. +pub struct JumpHopGalleryCardViewTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `jump_hop_gallery_card_view`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait JumpHopGalleryCardViewTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`JumpHopGalleryCardViewTableHandle`], which mediates access to the table `jump_hop_gallery_card_view`. + fn jump_hop_gallery_card_view(&self) -> JumpHopGalleryCardViewTableHandle<'_>; +} + +impl JumpHopGalleryCardViewTableAccess for super::RemoteTables { + fn jump_hop_gallery_card_view(&self) -> JumpHopGalleryCardViewTableHandle<'_> { + JumpHopGalleryCardViewTableHandle { + imp: self + .imp + .get_table::("jump_hop_gallery_card_view"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct JumpHopGalleryCardViewInsertCallbackId(__sdk::CallbackId); +pub struct JumpHopGalleryCardViewDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for JumpHopGalleryCardViewTableHandle<'ctx> { + type Row = JumpHopGalleryCardViewRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = JumpHopGalleryCardViewInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> JumpHopGalleryCardViewInsertCallbackId { + JumpHopGalleryCardViewInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: JumpHopGalleryCardViewInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = JumpHopGalleryCardViewDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> JumpHopGalleryCardViewDeleteCallbackId { + JumpHopGalleryCardViewDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: JumpHopGalleryCardViewDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = + client_cache.get_or_make_table::("jump_hop_gallery_card_view"); +} + +#[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 `JumpHopGalleryCardViewRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait jump_hop_gallery_card_viewQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `JumpHopGalleryCardViewRow`. + fn jump_hop_gallery_card_view( + &self, + ) -> __sdk::__query_builder::Table; +} + +impl jump_hop_gallery_card_viewQueryTableAccess for __sdk::QueryTableAccessor { + fn jump_hop_gallery_card_view( + &self, + ) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("jump_hop_gallery_card_view") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_gallery_view_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_gallery_view_row_type.rs new file mode 100644 index 00000000..cdf7e954 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_gallery_view_row_type.rs @@ -0,0 +1,116 @@ +// 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::jump_hop_character_asset_snapshot_type::JumpHopCharacterAssetSnapshot; +use super::jump_hop_path_type::JumpHopPath; +use super::jump_hop_tile_asset_snapshot_type::JumpHopTileAssetSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct JumpHopGalleryViewRow { + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: String, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + pub difficulty: String, + pub style_preset: String, + pub character_prompt: String, + pub tile_prompt: String, + pub end_mood_prompt: Option, + pub character_asset: Option, + pub tile_atlas_asset: Option, + pub tile_assets: Vec, + pub path: JumpHopPath, + pub cover_image_src: String, + pub cover_composite: Option, + pub publication_status: String, + pub publish_ready: bool, + pub play_count: u32, + pub generation_status: String, + pub updated_at_micros: i64, + pub published_at_micros: Option, +} + +impl __sdk::InModule for JumpHopGalleryViewRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `JumpHopGalleryViewRow`. +/// +/// Provides typed access to columns for query building. +pub struct JumpHopGalleryViewRowCols { + pub work_id: __sdk::__query_builder::Col, + pub profile_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub source_session_id: __sdk::__query_builder::Col, + pub author_display_name: __sdk::__query_builder::Col, + pub work_title: __sdk::__query_builder::Col, + pub work_description: __sdk::__query_builder::Col, + pub theme_tags: __sdk::__query_builder::Col>, + pub difficulty: __sdk::__query_builder::Col, + pub style_preset: __sdk::__query_builder::Col, + pub character_prompt: __sdk::__query_builder::Col, + pub tile_prompt: __sdk::__query_builder::Col, + pub end_mood_prompt: __sdk::__query_builder::Col>, + pub character_asset: + __sdk::__query_builder::Col>, + pub tile_atlas_asset: + __sdk::__query_builder::Col>, + pub tile_assets: + __sdk::__query_builder::Col>, + pub path: __sdk::__query_builder::Col, + pub cover_image_src: __sdk::__query_builder::Col, + pub cover_composite: __sdk::__query_builder::Col>, + pub publication_status: __sdk::__query_builder::Col, + pub publish_ready: __sdk::__query_builder::Col, + pub play_count: __sdk::__query_builder::Col, + pub generation_status: __sdk::__query_builder::Col, + pub updated_at_micros: __sdk::__query_builder::Col, + pub published_at_micros: __sdk::__query_builder::Col>, +} + +impl __sdk::__query_builder::HasCols for JumpHopGalleryViewRow { + type Cols = JumpHopGalleryViewRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + JumpHopGalleryViewRowCols { + work_id: __sdk::__query_builder::Col::new(table_name, "work_id"), + profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + source_session_id: __sdk::__query_builder::Col::new(table_name, "source_session_id"), + author_display_name: __sdk::__query_builder::Col::new( + table_name, + "author_display_name", + ), + work_title: __sdk::__query_builder::Col::new(table_name, "work_title"), + work_description: __sdk::__query_builder::Col::new(table_name, "work_description"), + theme_tags: __sdk::__query_builder::Col::new(table_name, "theme_tags"), + difficulty: __sdk::__query_builder::Col::new(table_name, "difficulty"), + style_preset: __sdk::__query_builder::Col::new(table_name, "style_preset"), + character_prompt: __sdk::__query_builder::Col::new(table_name, "character_prompt"), + tile_prompt: __sdk::__query_builder::Col::new(table_name, "tile_prompt"), + end_mood_prompt: __sdk::__query_builder::Col::new(table_name, "end_mood_prompt"), + character_asset: __sdk::__query_builder::Col::new(table_name, "character_asset"), + tile_atlas_asset: __sdk::__query_builder::Col::new(table_name, "tile_atlas_asset"), + tile_assets: __sdk::__query_builder::Col::new(table_name, "tile_assets"), + path: __sdk::__query_builder::Col::new(table_name, "path"), + cover_image_src: __sdk::__query_builder::Col::new(table_name, "cover_image_src"), + cover_composite: __sdk::__query_builder::Col::new(table_name, "cover_composite"), + publication_status: __sdk::__query_builder::Col::new(table_name, "publication_status"), + publish_ready: __sdk::__query_builder::Col::new(table_name, "publish_ready"), + play_count: __sdk::__query_builder::Col::new(table_name, "play_count"), + generation_status: __sdk::__query_builder::Col::new(table_name, "generation_status"), + updated_at_micros: __sdk::__query_builder::Col::new(table_name, "updated_at_micros"), + published_at_micros: __sdk::__query_builder::Col::new( + table_name, + "published_at_micros", + ), + } + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_gallery_view_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_gallery_view_table.rs new file mode 100644 index 00000000..c55683d1 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_gallery_view_table.rs @@ -0,0 +1,116 @@ +// 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::jump_hop_character_asset_snapshot_type::JumpHopCharacterAssetSnapshot; +use super::jump_hop_gallery_view_row_type::JumpHopGalleryViewRow; +use super::jump_hop_path_type::JumpHopPath; +use super::jump_hop_tile_asset_snapshot_type::JumpHopTileAssetSnapshot; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `jump_hop_gallery_view`. +/// +/// Obtain a handle from the [`JumpHopGalleryViewTableAccess::jump_hop_gallery_view`] method on [`super::RemoteTables`], +/// like `ctx.db.jump_hop_gallery_view()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.jump_hop_gallery_view().on_insert(...)`. +pub struct JumpHopGalleryViewTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `jump_hop_gallery_view`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait JumpHopGalleryViewTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`JumpHopGalleryViewTableHandle`], which mediates access to the table `jump_hop_gallery_view`. + fn jump_hop_gallery_view(&self) -> JumpHopGalleryViewTableHandle<'_>; +} + +impl JumpHopGalleryViewTableAccess for super::RemoteTables { + fn jump_hop_gallery_view(&self) -> JumpHopGalleryViewTableHandle<'_> { + JumpHopGalleryViewTableHandle { + imp: self + .imp + .get_table::("jump_hop_gallery_view"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct JumpHopGalleryViewInsertCallbackId(__sdk::CallbackId); +pub struct JumpHopGalleryViewDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for JumpHopGalleryViewTableHandle<'ctx> { + type Row = JumpHopGalleryViewRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = JumpHopGalleryViewInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> JumpHopGalleryViewInsertCallbackId { + JumpHopGalleryViewInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: JumpHopGalleryViewInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = JumpHopGalleryViewDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> JumpHopGalleryViewDeleteCallbackId { + JumpHopGalleryViewDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: JumpHopGalleryViewDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("jump_hop_gallery_view"); +} + +#[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 `JumpHopGalleryViewRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait jump_hop_gallery_viewQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `JumpHopGalleryViewRow`. + fn jump_hop_gallery_view(&self) -> __sdk::__query_builder::Table; +} + +impl jump_hop_gallery_viewQueryTableAccess for __sdk::QueryTableAccessor { + fn jump_hop_gallery_view(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("jump_hop_gallery_view") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_jump_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_jump_procedure.rs new file mode 100644 index 00000000..1535f96f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_jump_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::jump_hop_run_jump_input_type::JumpHopRunJumpInput; +use super::jump_hop_run_procedure_result_type::JumpHopRunProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct JumpHopJumpArgs { + pub input: JumpHopRunJumpInput, +} + +impl __sdk::InModule for JumpHopJumpArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `jump_hop_jump`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait jump_hop_jump { + fn jump_hop_jump(&self, input: JumpHopRunJumpInput) { + self.jump_hop_jump_then(input, |_, _| {}); + } + + fn jump_hop_jump_then( + &self, + input: JumpHopRunJumpInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl jump_hop_jump for super::RemoteProcedures { + fn jump_hop_jump_then( + &self, + input: JumpHopRunJumpInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, JumpHopRunProcedureResult>( + "jump_hop_jump", + JumpHopJumpArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_jump_result_kind_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_jump_result_kind_type.rs new file mode 100644 index 00000000..6db87bac --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_jump_result_kind_type.rs @@ -0,0 +1,22 @@ +// 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)] +#[derive(Copy, Eq, Hash)] +pub enum JumpHopJumpResultKind { + Miss, + + Hit, + + Perfect, + + Finish, +} + +impl __sdk::InModule for JumpHopJumpResultKind { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_last_jump_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_last_jump_type.rs new file mode 100644 index 00000000..5d8ef5bd --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_last_jump_type.rs @@ -0,0 +1,22 @@ +// 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::jump_hop_jump_result_kind_type::JumpHopJumpResultKind; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct JumpHopLastJump { + pub charge_ms: u32, + pub jump_distance: f32, + pub target_platform_index: u32, + pub landed_x: f32, + pub landed_y: f32, + pub result: JumpHopJumpResultKind, +} + +impl __sdk::InModule for JumpHopLastJump { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_path_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_path_type.rs new file mode 100644 index 00000000..b0ebd1f7 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_path_type.rs @@ -0,0 +1,24 @@ +// 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::jump_hop_difficulty_type::JumpHopDifficulty; +use super::jump_hop_platform_type::JumpHopPlatform; +use super::jump_hop_scoring_type::JumpHopScoring; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct JumpHopPath { + pub seed: String, + pub difficulty: JumpHopDifficulty, + pub platforms: Vec, + pub finish_index: u32, + pub camera_preset: String, + pub scoring: JumpHopScoring, +} + +impl __sdk::InModule for JumpHopPath { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_platform_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_platform_type.rs new file mode 100644 index 00000000..1aa60925 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_platform_type.rs @@ -0,0 +1,25 @@ +// 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::jump_hop_tile_type_type::JumpHopTileType; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct JumpHopPlatform { + pub platform_id: String, + pub tile_type: JumpHopTileType, + pub x: f32, + pub y: f32, + pub width: f32, + pub height: f32, + pub landing_radius: f32, + pub perfect_radius: f32, + pub score_value: u32, +} + +impl __sdk::InModule for JumpHopPlatform { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_get_input_type.rs new file mode 100644 index 00000000..7600f622 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_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 JumpHopRunGetInput { + pub run_id: String, + pub owner_user_id: String, +} + +impl __sdk::InModule for JumpHopRunGetInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_jump_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_jump_input_type.rs new file mode 100644 index 00000000..e73b5530 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_jump_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 JumpHopRunJumpInput { + pub run_id: String, + pub owner_user_id: String, + pub charge_ms: u32, + pub client_event_id: String, + pub jumped_at_ms: i64, +} + +impl __sdk::InModule for JumpHopRunJumpInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_procedure_result_type.rs new file mode 100644 index 00000000..c963bbbf --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_procedure_result_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}; + +use super::jump_hop_run_snapshot_type::JumpHopRunSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct JumpHopRunProcedureResult { + pub ok: bool, + pub run: Option, + pub error_message: Option, +} + +impl __sdk::InModule for JumpHopRunProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_restart_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_restart_input_type.rs new file mode 100644 index 00000000..dde3900f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_restart_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 JumpHopRunRestartInput { + pub source_run_id: String, + pub next_run_id: String, + pub owner_user_id: String, + pub client_action_id: String, + pub restarted_at_ms: i64, +} + +impl __sdk::InModule for JumpHopRunRestartInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_snapshot_type.rs new file mode 100644 index 00000000..e1402458 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_snapshot_type.rs @@ -0,0 +1,29 @@ +// 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::jump_hop_last_jump_type::JumpHopLastJump; +use super::jump_hop_path_type::JumpHopPath; +use super::jump_hop_run_status_type::JumpHopRunStatus; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct JumpHopRunSnapshot { + pub run_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub status: JumpHopRunStatus, + pub current_platform_index: u32, + pub score: u32, + pub combo: u32, + pub last_jump: Option, + pub started_at_ms: u64, + pub finished_at_ms: Option, + pub path: JumpHopPath, +} + +impl __sdk::InModule for JumpHopRunSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_start_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_start_input_type.rs new file mode 100644 index 00000000..40578dae --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_start_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 JumpHopRunStartInput { + pub run_id: String, + pub owner_user_id: String, + pub profile_id: String, + pub client_event_id: String, + pub started_at_ms: i64, +} + +impl __sdk::InModule for JumpHopRunStartInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_status_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_status_type.rs new file mode 100644 index 00000000..46993265 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_run_status_type.rs @@ -0,0 +1,20 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +#[derive(Copy, Eq, Hash)] +pub enum JumpHopRunStatus { + Playing, + + Failed, + + Cleared, +} + +impl __sdk::InModule for JumpHopRunStatus { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_runtime_run_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_runtime_run_row_type.rs new file mode 100644 index 00000000..64c5205f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_runtime_run_row_type.rs @@ -0,0 +1,89 @@ +// 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 JumpHopRuntimeRunRow { + pub run_id: String, + pub owner_user_id: String, + pub profile_id: String, + pub status: String, + pub started_at_ms: i64, + pub finished_at_ms: i64, + pub current_platform_index: u32, + pub score: u32, + pub combo: u32, + pub snapshot_json: String, + pub created_at: __sdk::Timestamp, + pub updated_at: __sdk::Timestamp, +} + +impl __sdk::InModule for JumpHopRuntimeRunRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `JumpHopRuntimeRunRow`. +/// +/// Provides typed access to columns for query building. +pub struct JumpHopRuntimeRunRowCols { + pub run_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub profile_id: __sdk::__query_builder::Col, + pub status: __sdk::__query_builder::Col, + pub started_at_ms: __sdk::__query_builder::Col, + pub finished_at_ms: __sdk::__query_builder::Col, + pub current_platform_index: __sdk::__query_builder::Col, + pub score: __sdk::__query_builder::Col, + pub combo: __sdk::__query_builder::Col, + pub snapshot_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 JumpHopRuntimeRunRow { + type Cols = JumpHopRuntimeRunRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + JumpHopRuntimeRunRowCols { + run_id: __sdk::__query_builder::Col::new(table_name, "run_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"), + status: __sdk::__query_builder::Col::new(table_name, "status"), + started_at_ms: __sdk::__query_builder::Col::new(table_name, "started_at_ms"), + finished_at_ms: __sdk::__query_builder::Col::new(table_name, "finished_at_ms"), + current_platform_index: __sdk::__query_builder::Col::new( + table_name, + "current_platform_index", + ), + score: __sdk::__query_builder::Col::new(table_name, "score"), + combo: __sdk::__query_builder::Col::new(table_name, "combo"), + snapshot_json: __sdk::__query_builder::Col::new(table_name, "snapshot_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 `JumpHopRuntimeRunRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct JumpHopRuntimeRunRowIxCols { + pub owner_user_id: __sdk::__query_builder::IxCol, + pub profile_id: __sdk::__query_builder::IxCol, + pub run_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for JumpHopRuntimeRunRow { + type IxCols = JumpHopRuntimeRunRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + JumpHopRuntimeRunRowIxCols { + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + profile_id: __sdk::__query_builder::IxCol::new(table_name, "profile_id"), + run_id: __sdk::__query_builder::IxCol::new(table_name, "run_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for JumpHopRuntimeRunRow {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_runtime_run_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_runtime_run_table.rs new file mode 100644 index 00000000..1fd4bab9 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_runtime_run_table.rs @@ -0,0 +1,161 @@ +// 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::jump_hop_runtime_run_row_type::JumpHopRuntimeRunRow; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `jump_hop_runtime_run`. +/// +/// Obtain a handle from the [`JumpHopRuntimeRunTableAccess::jump_hop_runtime_run`] method on [`super::RemoteTables`], +/// like `ctx.db.jump_hop_runtime_run()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.jump_hop_runtime_run().on_insert(...)`. +pub struct JumpHopRuntimeRunTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `jump_hop_runtime_run`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait JumpHopRuntimeRunTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`JumpHopRuntimeRunTableHandle`], which mediates access to the table `jump_hop_runtime_run`. + fn jump_hop_runtime_run(&self) -> JumpHopRuntimeRunTableHandle<'_>; +} + +impl JumpHopRuntimeRunTableAccess for super::RemoteTables { + fn jump_hop_runtime_run(&self) -> JumpHopRuntimeRunTableHandle<'_> { + JumpHopRuntimeRunTableHandle { + imp: self + .imp + .get_table::("jump_hop_runtime_run"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct JumpHopRuntimeRunInsertCallbackId(__sdk::CallbackId); +pub struct JumpHopRuntimeRunDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for JumpHopRuntimeRunTableHandle<'ctx> { + type Row = JumpHopRuntimeRunRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = JumpHopRuntimeRunInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> JumpHopRuntimeRunInsertCallbackId { + JumpHopRuntimeRunInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: JumpHopRuntimeRunInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = JumpHopRuntimeRunDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> JumpHopRuntimeRunDeleteCallbackId { + JumpHopRuntimeRunDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: JumpHopRuntimeRunDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct JumpHopRuntimeRunUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for JumpHopRuntimeRunTableHandle<'ctx> { + type UpdateCallbackId = JumpHopRuntimeRunUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> JumpHopRuntimeRunUpdateCallbackId { + JumpHopRuntimeRunUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: JumpHopRuntimeRunUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `run_id` unique index on the table `jump_hop_runtime_run`, +/// which allows point queries on the field of the same name +/// via the [`JumpHopRuntimeRunRunIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.jump_hop_runtime_run().run_id().find(...)`. +pub struct JumpHopRuntimeRunRunIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> JumpHopRuntimeRunTableHandle<'ctx> { + /// Get a handle on the `run_id` unique index on the table `jump_hop_runtime_run`. + pub fn run_id(&self) -> JumpHopRuntimeRunRunIdUnique<'ctx> { + JumpHopRuntimeRunRunIdUnique { + imp: self.imp.get_unique_constraint::("run_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> JumpHopRuntimeRunRunIdUnique<'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::("jump_hop_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 `JumpHopRuntimeRunRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait jump_hop_runtime_runQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `JumpHopRuntimeRunRow`. + fn jump_hop_runtime_run(&self) -> __sdk::__query_builder::Table; +} + +impl jump_hop_runtime_runQueryTableAccess for __sdk::QueryTableAccessor { + fn jump_hop_runtime_run(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("jump_hop_runtime_run") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_scoring_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_scoring_type.rs new file mode 100644 index 00000000..a33355b0 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_scoring_type.rs @@ -0,0 +1,18 @@ +// 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 JumpHopScoring { + pub charge_to_distance_ratio: f32, + pub max_charge_ms: u32, + pub hit_bonus: u32, + pub perfect_bonus: u32, +} + +impl __sdk::InModule for JumpHopScoring { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_tile_asset_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_tile_asset_snapshot_type.rs new file mode 100644 index 00000000..6874988f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_tile_asset_snapshot_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 JumpHopTileAssetSnapshot { + pub tile_type: String, + pub image_src: String, + pub image_object_key: String, + pub asset_object_id: String, + pub source_atlas_cell: String, + pub visual_width: u32, + pub visual_height: u32, + pub top_surface_radius: f32, + pub landing_radius: f32, +} + +impl __sdk::InModule for JumpHopTileAssetSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_tile_type_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_tile_type_type.rs new file mode 100644 index 00000000..f417ad5f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_tile_type_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)] +#[derive(Copy, Eq, Hash)] +pub enum JumpHopTileType { + Start, + + Normal, + + Target, + + Finish, + + Bonus, + + Accent, +} + +impl __sdk::InModule for JumpHopTileType { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_get_input_type.rs new file mode 100644 index 00000000..1e9fd0eb --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_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 JumpHopWorkGetInput { + pub profile_id: String, + pub owner_user_id: String, +} + +impl __sdk::InModule for JumpHopWorkGetInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_procedure_result_type.rs new file mode 100644 index 00000000..138ee818 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_procedure_result_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}; + +use super::jump_hop_work_snapshot_type::JumpHopWorkSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct JumpHopWorkProcedureResult { + pub ok: bool, + pub work: Option, + pub error_message: Option, +} + +impl __sdk::InModule for JumpHopWorkProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_profile_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_profile_row_type.rs new file mode 100644 index 00000000..b7bbd776 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_profile_row_type.rs @@ -0,0 +1,134 @@ +// 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 JumpHopWorkProfileRow { + pub profile_id: String, + pub work_id: String, + pub owner_user_id: String, + pub source_session_id: String, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub theme_tags_json: String, + pub difficulty: String, + pub style_preset: String, + pub character_prompt: String, + pub tile_prompt: String, + pub end_mood_prompt: String, + pub character_asset_json: String, + pub tile_atlas_asset_json: String, + pub tile_assets_json: String, + pub path_json: String, + pub cover_image_src: String, + pub cover_composite: String, + pub generation_status: String, + pub publication_status: String, + pub play_count: u32, + pub updated_at: __sdk::Timestamp, + pub published_at: Option<__sdk::Timestamp>, +} + +impl __sdk::InModule for JumpHopWorkProfileRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `JumpHopWorkProfileRow`. +/// +/// Provides typed access to columns for query building. +pub struct JumpHopWorkProfileRowCols { + pub profile_id: __sdk::__query_builder::Col, + pub work_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub source_session_id: __sdk::__query_builder::Col, + pub author_display_name: __sdk::__query_builder::Col, + pub work_title: __sdk::__query_builder::Col, + pub work_description: __sdk::__query_builder::Col, + pub theme_tags_json: __sdk::__query_builder::Col, + pub difficulty: __sdk::__query_builder::Col, + pub style_preset: __sdk::__query_builder::Col, + pub character_prompt: __sdk::__query_builder::Col, + pub tile_prompt: __sdk::__query_builder::Col, + pub end_mood_prompt: __sdk::__query_builder::Col, + pub character_asset_json: __sdk::__query_builder::Col, + pub tile_atlas_asset_json: __sdk::__query_builder::Col, + pub tile_assets_json: __sdk::__query_builder::Col, + pub path_json: __sdk::__query_builder::Col, + pub cover_image_src: __sdk::__query_builder::Col, + pub cover_composite: __sdk::__query_builder::Col, + pub generation_status: __sdk::__query_builder::Col, + pub publication_status: __sdk::__query_builder::Col, + pub play_count: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, + pub published_at: __sdk::__query_builder::Col>, +} + +impl __sdk::__query_builder::HasCols for JumpHopWorkProfileRow { + type Cols = JumpHopWorkProfileRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + JumpHopWorkProfileRowCols { + profile_id: __sdk::__query_builder::Col::new(table_name, "profile_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"), + source_session_id: __sdk::__query_builder::Col::new(table_name, "source_session_id"), + author_display_name: __sdk::__query_builder::Col::new( + table_name, + "author_display_name", + ), + work_title: __sdk::__query_builder::Col::new(table_name, "work_title"), + work_description: __sdk::__query_builder::Col::new(table_name, "work_description"), + theme_tags_json: __sdk::__query_builder::Col::new(table_name, "theme_tags_json"), + difficulty: __sdk::__query_builder::Col::new(table_name, "difficulty"), + style_preset: __sdk::__query_builder::Col::new(table_name, "style_preset"), + character_prompt: __sdk::__query_builder::Col::new(table_name, "character_prompt"), + tile_prompt: __sdk::__query_builder::Col::new(table_name, "tile_prompt"), + end_mood_prompt: __sdk::__query_builder::Col::new(table_name, "end_mood_prompt"), + character_asset_json: __sdk::__query_builder::Col::new( + table_name, + "character_asset_json", + ), + tile_atlas_asset_json: __sdk::__query_builder::Col::new( + table_name, + "tile_atlas_asset_json", + ), + tile_assets_json: __sdk::__query_builder::Col::new(table_name, "tile_assets_json"), + path_json: __sdk::__query_builder::Col::new(table_name, "path_json"), + cover_image_src: __sdk::__query_builder::Col::new(table_name, "cover_image_src"), + cover_composite: __sdk::__query_builder::Col::new(table_name, "cover_composite"), + generation_status: __sdk::__query_builder::Col::new(table_name, "generation_status"), + publication_status: __sdk::__query_builder::Col::new(table_name, "publication_status"), + play_count: __sdk::__query_builder::Col::new(table_name, "play_count"), + 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 `JumpHopWorkProfileRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct JumpHopWorkProfileRowIxCols { + pub owner_user_id: __sdk::__query_builder::IxCol, + pub profile_id: __sdk::__query_builder::IxCol, + pub publication_status: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for JumpHopWorkProfileRow { + type IxCols = JumpHopWorkProfileRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + JumpHopWorkProfileRowIxCols { + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + profile_id: __sdk::__query_builder::IxCol::new(table_name, "profile_id"), + publication_status: __sdk::__query_builder::IxCol::new( + table_name, + "publication_status", + ), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for JumpHopWorkProfileRow {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_profile_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_profile_table.rs new file mode 100644 index 00000000..cdc2c77f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_profile_table.rs @@ -0,0 +1,161 @@ +// 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::jump_hop_work_profile_row_type::JumpHopWorkProfileRow; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `jump_hop_work_profile`. +/// +/// Obtain a handle from the [`JumpHopWorkProfileTableAccess::jump_hop_work_profile`] method on [`super::RemoteTables`], +/// like `ctx.db.jump_hop_work_profile()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.jump_hop_work_profile().on_insert(...)`. +pub struct JumpHopWorkProfileTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `jump_hop_work_profile`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait JumpHopWorkProfileTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`JumpHopWorkProfileTableHandle`], which mediates access to the table `jump_hop_work_profile`. + fn jump_hop_work_profile(&self) -> JumpHopWorkProfileTableHandle<'_>; +} + +impl JumpHopWorkProfileTableAccess for super::RemoteTables { + fn jump_hop_work_profile(&self) -> JumpHopWorkProfileTableHandle<'_> { + JumpHopWorkProfileTableHandle { + imp: self + .imp + .get_table::("jump_hop_work_profile"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct JumpHopWorkProfileInsertCallbackId(__sdk::CallbackId); +pub struct JumpHopWorkProfileDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for JumpHopWorkProfileTableHandle<'ctx> { + type Row = JumpHopWorkProfileRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = JumpHopWorkProfileInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> JumpHopWorkProfileInsertCallbackId { + JumpHopWorkProfileInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: JumpHopWorkProfileInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = JumpHopWorkProfileDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> JumpHopWorkProfileDeleteCallbackId { + JumpHopWorkProfileDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: JumpHopWorkProfileDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct JumpHopWorkProfileUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for JumpHopWorkProfileTableHandle<'ctx> { + type UpdateCallbackId = JumpHopWorkProfileUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> JumpHopWorkProfileUpdateCallbackId { + JumpHopWorkProfileUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: JumpHopWorkProfileUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `profile_id` unique index on the table `jump_hop_work_profile`, +/// which allows point queries on the field of the same name +/// via the [`JumpHopWorkProfileProfileIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.jump_hop_work_profile().profile_id().find(...)`. +pub struct JumpHopWorkProfileProfileIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> JumpHopWorkProfileTableHandle<'ctx> { + /// Get a handle on the `profile_id` unique index on the table `jump_hop_work_profile`. + pub fn profile_id(&self) -> JumpHopWorkProfileProfileIdUnique<'ctx> { + JumpHopWorkProfileProfileIdUnique { + imp: self.imp.get_unique_constraint::("profile_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> JumpHopWorkProfileProfileIdUnique<'ctx> { + /// Find the subscribed row whose `profile_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::("jump_hop_work_profile"); + _table.add_unique_constraint::("profile_id", |row| &row.profile_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 `JumpHopWorkProfileRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait jump_hop_work_profileQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `JumpHopWorkProfileRow`. + fn jump_hop_work_profile(&self) -> __sdk::__query_builder::Table; +} + +impl jump_hop_work_profileQueryTableAccess for __sdk::QueryTableAccessor { + fn jump_hop_work_profile(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("jump_hop_work_profile") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_publish_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_publish_input_type.rs new file mode 100644 index 00000000..dc24525d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_publish_input_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 JumpHopWorkPublishInput { + pub profile_id: String, + pub owner_user_id: String, + pub published_at_micros: i64, +} + +impl __sdk::InModule for JumpHopWorkPublishInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_snapshot_type.rs new file mode 100644 index 00000000..bda718d0 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_snapshot_type.rs @@ -0,0 +1,43 @@ +// 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::jump_hop_character_asset_snapshot_type::JumpHopCharacterAssetSnapshot; +use super::jump_hop_path_type::JumpHopPath; +use super::jump_hop_tile_asset_snapshot_type::JumpHopTileAssetSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct JumpHopWorkSnapshot { + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: String, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + pub difficulty: String, + pub style_preset: String, + pub character_prompt: String, + pub tile_prompt: String, + pub end_mood_prompt: Option, + pub character_asset: Option, + pub tile_atlas_asset: Option, + pub tile_assets: Vec, + pub path: JumpHopPath, + pub cover_image_src: String, + pub cover_composite: Option, + pub publication_status: String, + pub publish_ready: bool, + pub play_count: u32, + pub generation_status: String, + pub updated_at_micros: i64, + pub published_at_micros: Option, +} + +impl __sdk::InModule for JumpHopWorkSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_update_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_update_input_type.rs new file mode 100644 index 00000000..6e587c2b --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_update_input_type.rs @@ -0,0 +1,24 @@ +// 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 JumpHopWorkUpdateInput { + pub profile_id: String, + pub owner_user_id: String, + pub work_title: String, + pub work_description: String, + pub theme_tags_json: String, + pub difficulty: Option, + pub style_preset: Option, + pub cover_image_src: Option, + pub cover_composite: Option, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for JumpHopWorkUpdateInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_works_list_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_works_list_input_type.rs new file mode 100644 index 00000000..c447cb67 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_works_list_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 JumpHopWorksListInput { + pub owner_user_id: String, + pub published_only: bool, +} + +impl __sdk::InModule for JumpHopWorksListInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_works_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_works_procedure_result_type.rs new file mode 100644 index 00000000..88d0daa9 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_works_procedure_result_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}; + +use super::jump_hop_work_snapshot_type::JumpHopWorkSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct JumpHopWorksProcedureResult { + pub ok: bool, + pub items: Vec, + pub error_message: Option, +} + +impl __sdk::InModule for JumpHopWorksProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/list_jump_hop_works_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/list_jump_hop_works_procedure.rs new file mode 100644 index 00000000..18b5cce5 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/list_jump_hop_works_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::jump_hop_works_list_input_type::JumpHopWorksListInput; +use super::jump_hop_works_procedure_result_type::JumpHopWorksProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct ListJumpHopWorksArgs { + pub input: JumpHopWorksListInput, +} + +impl __sdk::InModule for ListJumpHopWorksArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `list_jump_hop_works`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait list_jump_hop_works { + fn list_jump_hop_works(&self, input: JumpHopWorksListInput) { + self.list_jump_hop_works_then(input, |_, _| {}); + } + + fn list_jump_hop_works_then( + &self, + input: JumpHopWorksListInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl list_jump_hop_works for super::RemoteProcedures { + fn list_jump_hop_works_then( + &self, + input: JumpHopWorksListInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, JumpHopWorksProcedureResult>( + "list_jump_hop_works", + ListJumpHopWorksArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/publish_jump_hop_work_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/publish_jump_hop_work_procedure.rs new file mode 100644 index 00000000..926aed9b --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/publish_jump_hop_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::jump_hop_work_procedure_result_type::JumpHopWorkProcedureResult; +use super::jump_hop_work_publish_input_type::JumpHopWorkPublishInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct PublishJumpHopWorkArgs { + pub input: JumpHopWorkPublishInput, +} + +impl __sdk::InModule for PublishJumpHopWorkArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `publish_jump_hop_work`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait publish_jump_hop_work { + fn publish_jump_hop_work(&self, input: JumpHopWorkPublishInput) { + self.publish_jump_hop_work_then(input, |_, _| {}); + } + + fn publish_jump_hop_work_then( + &self, + input: JumpHopWorkPublishInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl publish_jump_hop_work for super::RemoteProcedures { + fn publish_jump_hop_work_then( + &self, + input: JumpHopWorkPublishInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, JumpHopWorkProcedureResult>( + "publish_jump_hop_work", + PublishJumpHopWorkArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_draft_level_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_draft_level_type.rs index 36f12999..d2b44606 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_draft_level_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_draft_level_type.rs @@ -17,6 +17,12 @@ pub struct PuzzleDraftLevel { pub ui_background_prompt: Option, pub ui_background_image_src: Option, pub ui_background_image_object_key: Option, + pub level_scene_image_src: Option, + pub level_scene_image_object_key: Option, + pub ui_spritesheet_image_src: Option, + pub ui_spritesheet_image_object_key: Option, + pub level_background_image_src: Option, + pub level_background_image_object_key: Option, pub background_music: Option, pub candidates: Vec, pub selected_candidate_id: Option, diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_runtime_level_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_runtime_level_snapshot_type.rs index 3554ed20..bce6df25 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_runtime_level_snapshot_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_runtime_level_snapshot_type.rs @@ -23,6 +23,10 @@ pub struct PuzzleRuntimeLevelSnapshot { pub cover_image_src: Option, pub ui_background_image_src: Option, pub ui_background_image_object_key: Option, + pub level_background_image_src: Option, + pub level_background_image_object_key: Option, + pub ui_spritesheet_image_src: Option, + pub ui_spritesheet_image_object_key: Option, pub background_music: Option, pub board: PuzzleBoardSnapshot, pub status: PuzzleRuntimeLevelStatus, diff --git a/server-rs/crates/spacetime-client/src/module_bindings/restart_jump_hop_run_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/restart_jump_hop_run_procedure.rs new file mode 100644 index 00000000..cde6daca --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/restart_jump_hop_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::jump_hop_run_procedure_result_type::JumpHopRunProcedureResult; +use super::jump_hop_run_restart_input_type::JumpHopRunRestartInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct RestartJumpHopRunArgs { + pub input: JumpHopRunRestartInput, +} + +impl __sdk::InModule for RestartJumpHopRunArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `restart_jump_hop_run`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait restart_jump_hop_run { + fn restart_jump_hop_run(&self, input: JumpHopRunRestartInput) { + self.restart_jump_hop_run_then(input, |_, _| {}); + } + + fn restart_jump_hop_run_then( + &self, + input: JumpHopRunRestartInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl restart_jump_hop_run for super::RemoteProcedures { + fn restart_jump_hop_run_then( + &self, + input: JumpHopRunRestartInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, JumpHopRunProcedureResult>( + "restart_jump_hop_run", + RestartJumpHopRunArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/start_jump_hop_run_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/start_jump_hop_run_procedure.rs new file mode 100644 index 00000000..7a52fc9f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/start_jump_hop_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::jump_hop_run_procedure_result_type::JumpHopRunProcedureResult; +use super::jump_hop_run_start_input_type::JumpHopRunStartInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct StartJumpHopRunArgs { + pub input: JumpHopRunStartInput, +} + +impl __sdk::InModule for StartJumpHopRunArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `start_jump_hop_run`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait start_jump_hop_run { + fn start_jump_hop_run(&self, input: JumpHopRunStartInput) { + self.start_jump_hop_run_then(input, |_, _| {}); + } + + fn start_jump_hop_run_then( + &self, + input: JumpHopRunStartInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl start_jump_hop_run for super::RemoteProcedures { + fn start_jump_hop_run_then( + &self, + input: JumpHopRunStartInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, JumpHopRunProcedureResult>( + "start_jump_hop_run", + StartJumpHopRunArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/update_jump_hop_work_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/update_jump_hop_work_procedure.rs new file mode 100644 index 00000000..9186048b --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/update_jump_hop_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::jump_hop_work_procedure_result_type::JumpHopWorkProcedureResult; +use super::jump_hop_work_update_input_type::JumpHopWorkUpdateInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct UpdateJumpHopWorkArgs { + pub input: JumpHopWorkUpdateInput, +} + +impl __sdk::InModule for UpdateJumpHopWorkArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `update_jump_hop_work`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait update_jump_hop_work { + fn update_jump_hop_work(&self, input: JumpHopWorkUpdateInput) { + self.update_jump_hop_work_then(input, |_, _| {}); + } + + fn update_jump_hop_work_then( + &self, + input: JumpHopWorkUpdateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl update_jump_hop_work for super::RemoteProcedures { + fn update_jump_hop_work_then( + &self, + input: JumpHopWorkUpdateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, JumpHopWorkProcedureResult>( + "update_jump_hop_work", + UpdateJumpHopWorkArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/runtime.rs b/server-rs/crates/spacetime-client/src/runtime.rs index 1b9429a7..08dbbf30 100644 --- a/server-rs/crates/spacetime-client/src/runtime.rs +++ b/server-rs/crates/spacetime-client/src/runtime.rs @@ -595,7 +595,18 @@ impl SpacetimeClient { let procedure_inputs = events .into_iter() - .map(crate::module_bindings::RuntimeTrackingEventInput::from) + .map(|event| crate::module_bindings::RuntimeTrackingEventInput { + event_id: event.event_id, + event_key: event.event_key, + scope_kind: map_runtime_tracking_scope_kind(event.scope_kind), + scope_id: event.scope_id, + user_id: event.user_id, + owner_user_id: event.owner_user_id, + profile_id: event.profile_id, + module_key: event.module_key, + metadata_json: event.metadata_json, + occurred_at_micros: event.occurred_at_micros, + }) .collect::>(); self.call_after_connect( diff --git a/server-rs/crates/spacetime-module/Cargo.toml b/server-rs/crates/spacetime-module/Cargo.toml index 17822404..16ff92e2 100644 --- a/server-rs/crates/spacetime-module/Cargo.toml +++ b/server-rs/crates/spacetime-module/Cargo.toml @@ -18,6 +18,7 @@ module-big-fish = { workspace = true, features = ["spacetime-types"] } module-combat = { workspace = true, features = ["spacetime-types"] } module-inventory = { workspace = true, features = ["spacetime-types"] } module-custom-world = { workspace = true, features = ["spacetime-types"] } +module-jump-hop = { workspace = true, features = ["spacetime-types"] } module-match3d = { workspace = true } module-npc = { workspace = true, features = ["spacetime-types"] } module-puzzle = { workspace = true, features = ["spacetime-types"] } diff --git a/server-rs/crates/spacetime-module/src/custom_world.rs b/server-rs/crates/spacetime-module/src/custom_world.rs index 36228bfe..a3249fa3 100644 --- a/server-rs/crates/spacetime-module/src/custom_world.rs +++ b/server-rs/crates/spacetime-module/src/custom_world.rs @@ -2562,6 +2562,18 @@ fn upsert_nested_result_profile_id( } } +fn resolve_publish_world_setting_text( + payload: &JsonMap, + draft_profile: &JsonMap, + session: &CustomWorldAgentSession, +) -> String { + module_custom_world::resolve_custom_world_publish_setting_text( + payload, + draft_profile, + &session.seed_text, + ) +} + fn is_same_agent_draft_profile_candidate( row: &CustomWorldProfile, owner_user_id: &str, @@ -2581,13 +2593,10 @@ fn execute_publish_world_action( ) -> Result { ensure_publishable_stage(session.stage, "publish_world")?; - let draft_profile = - if let Some(explicit) = payload.get("draftProfile").and_then(JsonValue::as_object) { - explicit.clone() - } else { - parse_optional_session_object(session.draft_profile_json.as_deref()) - .ok_or_else(|| "publish_world requires draft_profile_json".to_string())? - }; + // 中文注释:发布动作不再信任前端携带的 draftProfile。 + // 点击发布前,结果页 profile 必须先通过 sync_result_profile 写回 + // custom_world_agent_session.draft_profile_json;正式发布只读取这份会话真相。 + let draft_profile = read_publish_world_draft_profile_from_session(session)?; let gate = summarize_publish_gate_from_json( &session.session_id, session.stage, @@ -2601,24 +2610,9 @@ fn execute_publish_world_action( )); } - let profile_id = payload - .get("profileId") - .and_then(JsonValue::as_str) - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(ToOwned::to_owned) - .unwrap_or_else(|| gate.profile_id.clone()); - let setting_text = payload - .get("settingText") - .and_then(JsonValue::as_str) - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(ToOwned::to_owned) - .unwrap_or_else(|| session.seed_text.clone()); - let legacy_result_profile_json = payload - .get("legacyResultProfile") - .map(serialize_json_value) - .transpose()?; + let profile_id = gate.profile_id.clone(); + let setting_text = resolve_publish_world_setting_text(payload, &draft_profile, session); + let legacy_result_profile_json = None; let author_public_user_code = read_optional_text_field(payload, &["authorPublicUserCode"]) .unwrap_or_else(|| build_public_user_code_from_owner_user_id(&session.owner_user_id)); let author_display_name = read_optional_text_field(payload, &["authorDisplayName"]) @@ -2663,6 +2657,13 @@ fn execute_publish_world_action( Ok(build_custom_world_agent_operation_snapshot(&operation)) } +fn read_publish_world_draft_profile_from_session( + session: &CustomWorldAgentSession, +) -> Result, String> { + parse_optional_session_object(session.draft_profile_json.as_deref()) + .ok_or_else(|| "publish_world requires draft_profile_json".to_string()) +} + fn execute_revert_checkpoint_action( ctx: &ReducerContext, session: &CustomWorldAgentSession, @@ -5250,6 +5251,26 @@ mod tests { ); } + #[test] + fn publish_world_draft_profile_comes_from_session_not_payload() { + let session = build_test_custom_world_agent_session( + "seed", + RpgAgentStage::ReadyToPublish, + Some(r#"{"id":"saved-profile","name":"已保存草稿"}"#), + ); + let draft_profile = + read_publish_world_draft_profile_from_session(&session).expect("session draft exists"); + + assert_eq!( + draft_profile.get("id").and_then(JsonValue::as_str), + Some("saved-profile") + ); + assert_eq!( + draft_profile.get("name").and_then(JsonValue::as_str), + Some("已保存草稿") + ); + } + #[test] fn custom_world_agent_session_direct_work_content_ignores_empty_created_session() { let empty_session = diff --git a/server-rs/crates/spacetime-module/src/jump_hop.rs b/server-rs/crates/spacetime-module/src/jump_hop.rs new file mode 100644 index 00000000..d84c754c --- /dev/null +++ b/server-rs/crates/spacetime-module/src/jump_hop.rs @@ -0,0 +1,1165 @@ +pub(crate) mod tables; +mod types; + +pub use tables::*; +pub use types::*; + +use crate::{ProcedureContext, ReducerContext, SpacetimeType, Table, Timestamp}; +use module_jump_hop::{ + JumpHopDifficulty, JumpHopPath, JumpHopRunSnapshot, apply_jump, generate_jump_hop_path, + normalize_jump_hop_seed, parse_jump_hop_difficulty, restart_run, start_run, +}; +use serde::Serialize; +use serde::de::DeserializeOwned; +use spacetimedb::AnonymousViewContext; + +#[spacetimedb::view(accessor = jump_hop_gallery_view, public)] +pub fn jump_hop_gallery_view(ctx: &AnonymousViewContext) -> Vec { + let mut items = ctx + .db + .jump_hop_work_profile() + .by_jump_hop_work_publication_status() + .filter(JUMP_HOP_PUBLICATION_PUBLISHED) + .filter_map(|row| match build_gallery_view_row(&row) { + Ok(item) => Some(item), + Err(error) => { + log::warn!( + "跳一跳公开广场 view 跳过损坏的作品投影 profile_id={}: {}", + row.profile_id, + error + ); + None + } + }) + .collect::>(); + items.sort_by(|left, right| { + right + .updated_at_micros + .cmp(&left.updated_at_micros) + .then_with(|| left.profile_id.cmp(&right.profile_id)) + }); + items +} + +#[spacetimedb::view(accessor = jump_hop_gallery_card_view, public)] +pub fn jump_hop_gallery_card_view(ctx: &AnonymousViewContext) -> Vec { + jump_hop_gallery_view(ctx) + .into_iter() + .map(|row| JumpHopGalleryCardViewRow { + public_work_code: row.work_id.clone(), + work_id: row.work_id, + profile_id: row.profile_id, + owner_user_id: row.owner_user_id, + author_display_name: row.author_display_name, + work_title: row.work_title, + work_description: row.work_description, + theme_tags: row.theme_tags, + difficulty: row.difficulty, + style_preset: row.style_preset, + cover_image_src: row.cover_image_src, + publication_status: row.publication_status, + play_count: row.play_count, + updated_at_micros: row.updated_at_micros, + published_at_micros: row.published_at_micros, + generation_status: row.generation_status, + }) + .collect() +} + +#[derive(Clone, Debug, PartialEq, SpacetimeType)] +pub struct JumpHopGalleryViewRow { + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: String, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + pub difficulty: String, + pub style_preset: String, + pub character_prompt: String, + pub tile_prompt: String, + pub end_mood_prompt: Option, + pub character_asset: Option, + pub tile_atlas_asset: Option, + pub tile_assets: Vec, + pub path: JumpHopPath, + pub cover_image_src: String, + pub cover_composite: Option, + pub publication_status: String, + pub publish_ready: bool, + pub play_count: u32, + pub generation_status: String, + pub updated_at_micros: i64, + pub published_at_micros: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct JumpHopGalleryCardViewRow { + pub public_work_code: String, + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + pub difficulty: String, + pub style_preset: String, + pub cover_image_src: String, + pub publication_status: String, + pub play_count: u32, + pub updated_at_micros: i64, + pub published_at_micros: Option, + pub generation_status: String, +} + +#[spacetimedb::procedure] +pub fn create_jump_hop_agent_session( + ctx: &mut ProcedureContext, + input: JumpHopAgentSessionCreateInput, +) -> JumpHopAgentSessionProcedureResult { + match ctx.try_with_tx(|tx| create_jump_hop_agent_session_tx(tx, input.clone())) { + Ok(session) => session_result(session), + Err(message) => session_error(message), + } +} + +#[spacetimedb::procedure] +pub fn get_jump_hop_agent_session( + ctx: &mut ProcedureContext, + input: JumpHopAgentSessionGetInput, +) -> JumpHopAgentSessionProcedureResult { + match ctx.try_with_tx(|tx| get_jump_hop_agent_session_tx(tx, input.clone())) { + Ok(session) => session_result(session), + Err(message) => session_error(message), + } +} + +#[spacetimedb::procedure] +pub fn compile_jump_hop_draft( + ctx: &mut ProcedureContext, + input: JumpHopDraftCompileInput, +) -> JumpHopAgentSessionProcedureResult { + match ctx.try_with_tx(|tx| compile_jump_hop_draft_tx(tx, input.clone())) { + Ok(session) => session_result(session), + Err(message) => session_error(message), + } +} + +#[spacetimedb::procedure] +pub fn get_jump_hop_work_profile( + ctx: &mut ProcedureContext, + input: JumpHopWorkGetInput, +) -> JumpHopWorkProcedureResult { + match ctx.try_with_tx(|tx| get_jump_hop_work_profile_tx(tx, input.clone())) { + Ok(work) => work_result(work), + Err(message) => work_error(message), + } +} + +#[spacetimedb::procedure] +pub fn update_jump_hop_work( + ctx: &mut ProcedureContext, + input: JumpHopWorkUpdateInput, +) -> JumpHopWorkProcedureResult { + match ctx.try_with_tx(|tx| update_jump_hop_work_tx(tx, input.clone())) { + Ok(work) => work_result(work), + Err(message) => work_error(message), + } +} + +#[spacetimedb::procedure] +pub fn publish_jump_hop_work( + ctx: &mut ProcedureContext, + input: JumpHopWorkPublishInput, +) -> JumpHopWorkProcedureResult { + match ctx.try_with_tx(|tx| publish_jump_hop_work_tx(tx, input.clone())) { + Ok(work) => work_result(work), + Err(message) => work_error(message), + } +} + +#[spacetimedb::procedure] +pub fn list_jump_hop_works( + ctx: &mut ProcedureContext, + input: JumpHopWorksListInput, +) -> JumpHopWorksProcedureResult { + match ctx.try_with_tx(|tx| list_jump_hop_works_tx(tx, input.clone())) { + Ok(items) => JumpHopWorksProcedureResult { + ok: true, + items, + error_message: None, + }, + Err(message) => JumpHopWorksProcedureResult { + ok: false, + items: Vec::new(), + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn start_jump_hop_run( + ctx: &mut ProcedureContext, + input: JumpHopRunStartInput, +) -> JumpHopRunProcedureResult { + match ctx.try_with_tx(|tx| start_jump_hop_run_tx(tx, input.clone())) { + Ok(run) => run_result(run), + Err(message) => run_error(message), + } +} + +#[spacetimedb::procedure] +pub fn get_jump_hop_run( + ctx: &mut ProcedureContext, + input: JumpHopRunGetInput, +) -> JumpHopRunProcedureResult { + match ctx.try_with_tx(|tx| get_jump_hop_run_tx(tx, input.clone())) { + Ok(run) => run_result(run), + Err(message) => run_error(message), + } +} + +#[spacetimedb::procedure] +pub fn jump_hop_jump( + ctx: &mut ProcedureContext, + input: JumpHopRunJumpInput, +) -> JumpHopRunProcedureResult { + match ctx.try_with_tx(|tx| jump_hop_jump_tx(tx, input.clone())) { + Ok(run) => run_result(run), + Err(message) => run_error(message), + } +} + +#[spacetimedb::procedure] +pub fn restart_jump_hop_run( + ctx: &mut ProcedureContext, + input: JumpHopRunRestartInput, +) -> JumpHopRunProcedureResult { + match ctx.try_with_tx(|tx| restart_jump_hop_run_tx(tx, input.clone())) { + Ok(run) => run_result(run), + Err(message) => run_error(message), + } +} + +fn create_jump_hop_agent_session_tx( + ctx: &ReducerContext, + input: JumpHopAgentSessionCreateInput, +) -> Result { + require_non_empty(&input.session_id, "jump_hop session_id")?; + require_non_empty(&input.owner_user_id, "jump_hop owner_user_id")?; + if ctx + .db + .jump_hop_agent_session() + .session_id() + .find(&input.session_id) + .is_some() + { + return Err("jump_hop_agent_session.session_id 已存在".to_string()); + } + + let created_at = Timestamp::from_micros_since_unix_epoch(input.created_at_micros); + let config = input + .config_json + .as_deref() + .map(parse_config) + .transpose()? + .unwrap_or_else(|| default_config_from_seed(&input.seed_text)); + let draft = JumpHopDraftSnapshot { + template_id: JUMP_HOP_TEMPLATE_ID.to_string(), + template_name: JUMP_HOP_TEMPLATE_NAME.to_string(), + profile_id: None, + work_title: input.work_title.clone(), + work_description: input.work_description.clone(), + theme_tags: parse_tags(input.theme_tags_json.as_deref().unwrap_or("[]"))?, + difficulty: config.difficulty.clone(), + style_preset: config.style_preset.clone(), + character_prompt: config.character_prompt.clone(), + tile_prompt: config.tile_prompt.clone(), + end_mood_prompt: clean_optional(&config.end_mood_prompt), + character_asset: None, + tile_atlas_asset: None, + tile_assets: Vec::new(), + path: None, + cover_composite: None, + generation_status: JUMP_HOP_GENERATION_DRAFT.to_string(), + }; + ctx.db + .jump_hop_agent_session() + .insert(JumpHopAgentSessionRow { + session_id: input.session_id.clone(), + owner_user_id: input.owner_user_id.clone(), + seed_text: input.seed_text.trim().to_string(), + current_turn: 0, + progress_percent: 0, + stage: JUMP_HOP_STAGE_COLLECTING.to_string(), + config_json: to_json_string(&config), + draft_json: to_json_string(&draft), + last_assistant_reply: input.welcome_message_text.trim().to_string(), + published_profile_id: String::new(), + created_at, + updated_at: created_at, + }); + + get_jump_hop_agent_session_tx( + ctx, + JumpHopAgentSessionGetInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + }, + ) +} + +fn get_jump_hop_agent_session_tx( + ctx: &ReducerContext, + input: JumpHopAgentSessionGetInput, +) -> Result { + let row = find_owned_session(ctx, &input.session_id, &input.owner_user_id)?; + build_session_snapshot(&row) +} + +fn compile_jump_hop_draft_tx( + ctx: &ReducerContext, + input: JumpHopDraftCompileInput, +) -> Result { + require_non_empty(&input.profile_id, "jump_hop profile_id")?; + let session = find_owned_session(ctx, &input.session_id, &input.owner_user_id)?; + let mut config = parse_config(&session.config_json)?; + apply_compile_overrides(&mut config, &input)?; + + let seed = normalize_jump_hop_seed(&input.seed_text, &session.seed_text); + let path = generate_jump_hop_path(&seed, parse_jump_hop_difficulty(&config.difficulty)); + let tags = parse_tags(input.theme_tags_json.as_deref().unwrap_or("[]"))?; + let draft = JumpHopDraftSnapshot { + template_id: JUMP_HOP_TEMPLATE_ID.to_string(), + template_name: JUMP_HOP_TEMPLATE_NAME.to_string(), + profile_id: Some(input.profile_id.clone()), + work_title: clean_string(&input.work_title, "跳一跳作品"), + work_description: input.work_description.trim().to_string(), + theme_tags: tags.clone(), + difficulty: config.difficulty.clone(), + style_preset: config.style_preset.clone(), + character_prompt: config.character_prompt.clone(), + tile_prompt: config.tile_prompt.clone(), + end_mood_prompt: clean_optional(&config.end_mood_prompt), + character_asset: input + .character_asset_json + .as_deref() + .map(parse_json) + .transpose()?, + tile_atlas_asset: input + .tile_atlas_asset_json + .as_deref() + .map(parse_json) + .transpose()?, + tile_assets: input + .tile_assets_json + .as_deref() + .map(parse_json) + .transpose()? + .unwrap_or_default(), + path: Some(path.clone()), + cover_composite: input.cover_composite.as_deref().and_then(clean_optional), + generation_status: input + .generation_status + .clone() + .unwrap_or_else(|| JUMP_HOP_GENERATION_READY.to_string()), + }; + let compiled_at = Timestamp::from_micros_since_unix_epoch(input.compiled_at_micros); + let row = JumpHopWorkProfileRow { + profile_id: input.profile_id.clone(), + work_id: input.profile_id.clone(), + owner_user_id: input.owner_user_id.clone(), + source_session_id: input.session_id.clone(), + author_display_name: clean_string(&input.author_display_name, "跳一跳玩家"), + work_title: draft.work_title.clone(), + work_description: draft.work_description.clone(), + theme_tags_json: to_json_string(&tags), + difficulty: draft.difficulty.clone(), + style_preset: draft.style_preset.clone(), + character_prompt: draft.character_prompt.clone(), + tile_prompt: draft.tile_prompt.clone(), + end_mood_prompt: draft.end_mood_prompt.clone().unwrap_or_default(), + character_asset_json: draft + .character_asset + .as_ref() + .map(to_json_string) + .unwrap_or_default(), + tile_atlas_asset_json: draft + .tile_atlas_asset + .as_ref() + .map(to_json_string) + .unwrap_or_default(), + tile_assets_json: to_json_string(&draft.tile_assets), + path_json: to_json_string(&path), + cover_image_src: draft.cover_composite.clone().unwrap_or_default(), + cover_composite: draft.cover_composite.clone().unwrap_or_default(), + generation_status: draft.generation_status.clone(), + publication_status: JUMP_HOP_PUBLICATION_DRAFT.to_string(), + play_count: 0, + updated_at: compiled_at, + published_at: None, + }; + upsert_work(ctx, row); + replace_session( + ctx, + &session, + JumpHopAgentSessionRow { + progress_percent: 100, + stage: JUMP_HOP_STAGE_DRAFT_COMPILED.to_string(), + config_json: to_json_string(&config), + draft_json: to_json_string(&draft), + published_profile_id: input.profile_id, + last_assistant_reply: "跳一跳草稿已生成,可以进入结果页试玩和发布。".to_string(), + updated_at: compiled_at, + ..clone_session(&session) + }, + ); + + get_jump_hop_agent_session_tx( + ctx, + JumpHopAgentSessionGetInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + }, + ) +} + +fn get_jump_hop_work_profile_tx( + ctx: &ReducerContext, + input: JumpHopWorkGetInput, +) -> Result { + let row = find_work(ctx, &input.profile_id)?; + if !input.owner_user_id.trim().is_empty() && row.owner_user_id != input.owner_user_id { + return Err("无权访问该 jump_hop work".to_string()); + } + build_work_snapshot(&row) +} + +fn update_jump_hop_work_tx( + ctx: &ReducerContext, + input: JumpHopWorkUpdateInput, +) -> Result { + let row = find_owned_work(ctx, &input.profile_id, &input.owner_user_id)?; + let updated_at = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros); + let mut next = clone_work(&row); + next.work_title = clean_string(&input.work_title, &row.work_title); + next.work_description = input.work_description.trim().to_string(); + next.theme_tags_json = input.theme_tags_json.clone(); + if let Some(difficulty) = input.difficulty.as_deref().and_then(clean_optional) { + next.difficulty = difficulty; + let path = generate_jump_hop_path( + &normalize_jump_hop_seed(&row.profile_id, &row.source_session_id), + parse_jump_hop_difficulty(&next.difficulty), + ); + next.path_json = to_json_string(&path); + } + if let Some(style_preset) = input.style_preset.as_deref().and_then(clean_optional) { + next.style_preset = style_preset; + } + if let Some(cover) = input.cover_image_src.as_deref().and_then(clean_optional) { + next.cover_image_src = cover; + } + if let Some(cover) = input.cover_composite.as_deref().and_then(clean_optional) { + next.cover_composite = cover; + } + next.updated_at = updated_at; + replace_work(ctx, &row, next); + let updated = find_work(ctx, &row.profile_id)?; + sync_session_from_work_update(ctx, &updated, updated_at)?; + build_work_snapshot(&updated) +} + +fn publish_jump_hop_work_tx( + ctx: &ReducerContext, + input: JumpHopWorkPublishInput, +) -> Result { + let row = find_owned_work(ctx, &input.profile_id, &input.owner_user_id)?; + let published_at = Timestamp::from_micros_since_unix_epoch(input.published_at_micros); + replace_work( + ctx, + &row, + JumpHopWorkProfileRow { + publication_status: JUMP_HOP_PUBLICATION_PUBLISHED.to_string(), + updated_at: published_at, + published_at: Some(published_at), + ..clone_work(&row) + }, + ); + if let Some(session) = ctx + .db + .jump_hop_agent_session() + .session_id() + .find(&row.source_session_id) + { + replace_session( + ctx, + &session, + JumpHopAgentSessionRow { + stage: JUMP_HOP_STAGE_PUBLISHED.to_string(), + updated_at: published_at, + ..clone_session(&session) + }, + ); + } + let updated = find_work(ctx, &row.profile_id)?; + build_work_snapshot(&updated) +} + +fn list_jump_hop_works_tx( + ctx: &ReducerContext, + input: JumpHopWorksListInput, +) -> Result, String> { + let mut rows = if input.owner_user_id.trim().is_empty() { + ctx.db.jump_hop_work_profile().iter().collect::>() + } else { + ctx.db + .jump_hop_work_profile() + .by_jump_hop_work_owner_user_id() + .filter(input.owner_user_id.as_str()) + .collect::>() + }; + if input.published_only { + rows.retain(|row| row.publication_status == JUMP_HOP_PUBLICATION_PUBLISHED); + } + rows.sort_by(|left, right| { + right + .updated_at + .cmp(&left.updated_at) + .then_with(|| left.profile_id.cmp(&right.profile_id)) + }); + rows.into_iter() + .map(|row| build_work_snapshot(&row)) + .collect() +} + +fn start_jump_hop_run_tx( + ctx: &ReducerContext, + input: JumpHopRunStartInput, +) -> Result { + require_non_empty(&input.run_id, "jump_hop run_id")?; + let work = find_work(ctx, &input.profile_id)?; + let path = parse_json::(&work.path_json)?; + let domain_run = start_run( + input.run_id.clone(), + input.owner_user_id.clone(), + input.profile_id.clone(), + path, + input.started_at_ms as u64, + ) + .map_err(|error| error.to_string())?; + let snapshot = domain_run; + upsert_run(ctx, &snapshot, input.started_at_ms); + increment_work_play_count(ctx, &work, input.started_at_ms); + insert_event( + ctx, + input.client_event_id, + input.owner_user_id, + input.profile_id, + input.run_id, + JUMP_HOP_EVENT_RUN_STARTED, + None, + input.started_at_ms, + ); + Ok(snapshot) +} + +fn get_jump_hop_run_tx( + ctx: &ReducerContext, + input: JumpHopRunGetInput, +) -> Result { + let row = find_owned_run(ctx, &input.run_id, &input.owner_user_id)?; + parse_json(&row.snapshot_json) +} + +fn jump_hop_jump_tx( + ctx: &ReducerContext, + input: JumpHopRunJumpInput, +) -> Result { + let row = find_owned_run(ctx, &input.run_id, &input.owner_user_id)?; + let snapshot = parse_json::(&row.snapshot_json)?; + let domain_next = apply_jump(&snapshot, input.charge_ms, input.jumped_at_ms as u64) + .map_err(|error| error.to_string())?; + let next = domain_next; + replace_run(ctx, &row, &next, input.jumped_at_ms); + insert_event( + ctx, + input.client_event_id, + input.owner_user_id, + next.profile_id.clone(), + input.run_id, + JUMP_HOP_EVENT_JUMP, + next.last_jump + .as_ref() + .map(|jump| jump.result.as_str().to_string()) + .or_else(|| Some(next.status.as_str().to_string())), + input.jumped_at_ms, + ); + Ok(next) +} + +fn restart_jump_hop_run_tx( + ctx: &ReducerContext, + input: JumpHopRunRestartInput, +) -> Result { + let source = find_owned_run(ctx, &input.source_run_id, &input.owner_user_id)?; + let snapshot = parse_json::(&source.snapshot_json)?; + let domain_next = restart_run( + &snapshot, + input.next_run_id.clone(), + input.restarted_at_ms as u64, + ) + .map_err(|error| error.to_string())?; + let next = domain_next; + upsert_run(ctx, &next, input.restarted_at_ms); + insert_event( + ctx, + input.client_action_id, + input.owner_user_id, + next.profile_id.clone(), + input.next_run_id, + JUMP_HOP_EVENT_RUN_RESTARTED, + None, + input.restarted_at_ms, + ); + Ok(next) +} + +fn build_gallery_view_row(row: &JumpHopWorkProfileRow) -> Result { + let work = build_work_snapshot(row)?; + Ok(JumpHopGalleryViewRow { + work_id: work.work_id, + profile_id: work.profile_id, + owner_user_id: work.owner_user_id, + source_session_id: work.source_session_id, + author_display_name: work.author_display_name, + work_title: work.work_title, + work_description: work.work_description, + theme_tags: work.theme_tags, + difficulty: work.difficulty, + style_preset: work.style_preset, + character_prompt: work.character_prompt, + tile_prompt: work.tile_prompt, + end_mood_prompt: work.end_mood_prompt, + character_asset: work.character_asset, + tile_atlas_asset: work.tile_atlas_asset, + tile_assets: work.tile_assets, + path: work.path, + cover_image_src: work.cover_image_src, + cover_composite: work.cover_composite, + publication_status: work.publication_status, + publish_ready: work.publish_ready, + play_count: work.play_count, + generation_status: work.generation_status, + updated_at_micros: work.updated_at_micros, + published_at_micros: work.published_at_micros, + }) +} + +fn build_session_snapshot( + row: &JumpHopAgentSessionRow, +) -> Result { + Ok(JumpHopAgentSessionSnapshot { + session_id: row.session_id.clone(), + owner_user_id: row.owner_user_id.clone(), + seed_text: row.seed_text.clone(), + current_turn: row.current_turn, + progress_percent: row.progress_percent, + stage: row.stage.clone(), + config: parse_config(&row.config_json)?, + draft: clean_optional(&row.draft_json) + .map(|value| parse_json(&value)) + .transpose()?, + last_assistant_reply: row.last_assistant_reply.clone(), + published_profile_id: clean_optional(&row.published_profile_id), + created_at_micros: row.created_at.to_micros_since_unix_epoch(), + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + }) +} + +fn build_work_snapshot(row: &JumpHopWorkProfileRow) -> Result { + let path = parse_json(&row.path_json)?; + Ok(JumpHopWorkSnapshot { + work_id: row.work_id.clone(), + profile_id: row.profile_id.clone(), + owner_user_id: row.owner_user_id.clone(), + source_session_id: row.source_session_id.clone(), + author_display_name: row.author_display_name.clone(), + work_title: row.work_title.clone(), + work_description: row.work_description.clone(), + theme_tags: parse_tags(&row.theme_tags_json)?, + difficulty: row.difficulty.clone(), + style_preset: row.style_preset.clone(), + character_prompt: row.character_prompt.clone(), + tile_prompt: row.tile_prompt.clone(), + end_mood_prompt: clean_optional(&row.end_mood_prompt), + character_asset: clean_optional(&row.character_asset_json) + .map(|value| parse_json(&value)) + .transpose()?, + tile_atlas_asset: clean_optional(&row.tile_atlas_asset_json) + .map(|value| parse_json(&value)) + .transpose()?, + tile_assets: parse_json_or_default(&row.tile_assets_json), + path, + cover_image_src: row.cover_image_src.clone(), + cover_composite: clean_optional(&row.cover_composite), + publication_status: row.publication_status.clone(), + publish_ready: is_publish_ready(row), + play_count: row.play_count, + generation_status: row.generation_status.clone(), + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + published_at_micros: row + .published_at + .map(|value| value.to_micros_since_unix_epoch()), + }) +} + +fn sync_session_from_work_update( + ctx: &ReducerContext, + work: &JumpHopWorkProfileRow, + updated_at: Timestamp, +) -> Result<(), String> { + let Some(session) = ctx + .db + .jump_hop_agent_session() + .session_id() + .find(&work.source_session_id) + else { + return Ok(()); + }; + + let mut config = parse_config(&session.config_json)?; + config.theme_text = work.work_title.clone(); + config.difficulty = work.difficulty.clone(); + config.style_preset = work.style_preset.clone(); + config.character_prompt = work.character_prompt.clone(); + config.tile_prompt = work.tile_prompt.clone(); + config.end_mood_prompt = work.end_mood_prompt.clone(); + + let draft = JumpHopDraftSnapshot { + template_id: JUMP_HOP_TEMPLATE_ID.to_string(), + template_name: JUMP_HOP_TEMPLATE_NAME.to_string(), + profile_id: Some(work.profile_id.clone()), + work_title: work.work_title.clone(), + work_description: work.work_description.clone(), + theme_tags: parse_tags(&work.theme_tags_json)?, + difficulty: work.difficulty.clone(), + style_preset: work.style_preset.clone(), + character_prompt: work.character_prompt.clone(), + tile_prompt: work.tile_prompt.clone(), + end_mood_prompt: clean_optional(&work.end_mood_prompt), + character_asset: clean_optional(&work.character_asset_json) + .map(|value| parse_json(&value)) + .transpose()?, + tile_atlas_asset: clean_optional(&work.tile_atlas_asset_json) + .map(|value| parse_json(&value)) + .transpose()?, + tile_assets: parse_json_or_default(&work.tile_assets_json), + path: Some(parse_json(&work.path_json)?), + cover_composite: clean_optional(&work.cover_composite), + generation_status: work.generation_status.clone(), + }; + + replace_session( + ctx, + &session, + JumpHopAgentSessionRow { + config_json: to_json_string(&config), + draft_json: to_json_string(&draft), + updated_at, + ..clone_session(&session) + }, + ); + Ok(()) +} + +fn find_owned_session( + ctx: &ReducerContext, + session_id: &str, + owner_user_id: &str, +) -> Result { + let row = ctx + .db + .jump_hop_agent_session() + .session_id() + .find(&session_id.to_string()) + .ok_or_else(|| "jump_hop_agent_session 不存在".to_string())?; + if row.owner_user_id != owner_user_id { + return Err("无权访问该 jump_hop session".to_string()); + } + Ok(row) +} + +fn find_work(ctx: &ReducerContext, profile_id: &str) -> Result { + ctx.db + .jump_hop_work_profile() + .profile_id() + .find(&profile_id.to_string()) + .ok_or_else(|| "jump_hop_work_profile 不存在".to_string()) +} + +fn find_owned_work( + ctx: &ReducerContext, + profile_id: &str, + owner_user_id: &str, +) -> Result { + let row = find_work(ctx, profile_id)?; + if row.owner_user_id != owner_user_id { + return Err("无权访问该 jump_hop work".to_string()); + } + Ok(row) +} + +fn find_owned_run( + ctx: &ReducerContext, + run_id: &str, + owner_user_id: &str, +) -> Result { + let row = ctx + .db + .jump_hop_runtime_run() + .run_id() + .find(&run_id.to_string()) + .ok_or_else(|| "jump_hop_runtime_run 不存在".to_string())?; + if row.owner_user_id != owner_user_id { + return Err("无权访问该 jump_hop run".to_string()); + } + Ok(row) +} + +fn upsert_work(ctx: &ReducerContext, row: JumpHopWorkProfileRow) { + if let Some(old) = ctx + .db + .jump_hop_work_profile() + .profile_id() + .find(&row.profile_id) + { + ctx.db.jump_hop_work_profile().delete(old); + } + ctx.db.jump_hop_work_profile().insert(row); +} + +fn replace_work(ctx: &ReducerContext, old: &JumpHopWorkProfileRow, next: JumpHopWorkProfileRow) { + ctx.db.jump_hop_work_profile().delete(clone_work(old)); + ctx.db.jump_hop_work_profile().insert(next); +} + +fn replace_session( + ctx: &ReducerContext, + old: &JumpHopAgentSessionRow, + next: JumpHopAgentSessionRow, +) { + ctx.db.jump_hop_agent_session().delete(clone_session(old)); + ctx.db.jump_hop_agent_session().insert(next); +} + +fn upsert_run(ctx: &ReducerContext, snapshot: &JumpHopRunSnapshot, updated_at_ms: i64) { + if let Some(old) = ctx + .db + .jump_hop_runtime_run() + .run_id() + .find(&snapshot.run_id) + { + ctx.db.jump_hop_runtime_run().delete(old); + } + let created_at = Timestamp::from_micros_since_unix_epoch(updated_at_ms.saturating_mul(1000)); + ctx.db + .jump_hop_runtime_run() + .insert(run_row_from_snapshot(snapshot, created_at, created_at)); +} + +fn replace_run( + ctx: &ReducerContext, + old: &JumpHopRuntimeRunRow, + snapshot: &JumpHopRunSnapshot, + updated_at_ms: i64, +) { + ctx.db.jump_hop_runtime_run().delete(clone_run(old)); + ctx.db.jump_hop_runtime_run().insert(run_row_from_snapshot( + snapshot, + old.created_at, + Timestamp::from_micros_since_unix_epoch(updated_at_ms.saturating_mul(1000)), + )); +} + +fn run_row_from_snapshot( + snapshot: &JumpHopRunSnapshot, + created_at: Timestamp, + updated_at: Timestamp, +) -> JumpHopRuntimeRunRow { + JumpHopRuntimeRunRow { + run_id: snapshot.run_id.clone(), + owner_user_id: snapshot.owner_user_id.clone(), + profile_id: snapshot.profile_id.clone(), + status: snapshot.status.as_str().to_string(), + started_at_ms: snapshot.started_at_ms as i64, + finished_at_ms: snapshot + .finished_at_ms + .map(|value| value as i64) + .unwrap_or(0), + current_platform_index: snapshot.current_platform_index, + score: snapshot.score, + combo: snapshot.combo, + snapshot_json: to_json_string(snapshot), + created_at, + updated_at, + } +} + +fn increment_work_play_count(ctx: &ReducerContext, row: &JumpHopWorkProfileRow, played_at_ms: i64) { + replace_work( + ctx, + row, + JumpHopWorkProfileRow { + play_count: row.play_count.saturating_add(1), + updated_at: Timestamp::from_micros_since_unix_epoch(played_at_ms.saturating_mul(1000)), + ..clone_work(row) + }, + ); +} + +fn insert_event( + ctx: &ReducerContext, + event_id: String, + owner_user_id: String, + profile_id: String, + run_id: String, + event_type: &str, + result: Option, + occurred_at_ms: i64, +) { + let event_id = clean_optional(&event_id).unwrap_or_else(|| { + format!( + "jump-hop-event-{}-{}-{}", + run_id, event_type, occurred_at_ms + ) + }); + if ctx.db.jump_hop_event().event_id().find(&event_id).is_some() { + return; + } + ctx.db.jump_hop_event().insert(JumpHopEventRow { + event_id, + owner_user_id, + profile_id, + run_id, + event_type: event_type.to_string(), + result: result.unwrap_or_default(), + occurred_at: Timestamp::from_micros_since_unix_epoch(occurred_at_ms.saturating_mul(1000)), + }); +} + +fn is_publish_ready(row: &JumpHopWorkProfileRow) -> bool { + !row.work_title.trim().is_empty() + && !row.character_asset_json.trim().is_empty() + && !row.tile_atlas_asset_json.trim().is_empty() + && !row.tile_assets_json.trim().is_empty() + && !row.path_json.trim().is_empty() +} + +fn default_config_from_seed(seed_text: &str) -> JumpHopCreatorConfigSnapshot { + let seed = clean_string(seed_text, "跳一跳"); + JumpHopCreatorConfigSnapshot { + theme_text: seed.clone(), + difficulty: JumpHopDifficulty::Standard.as_str().to_string(), + style_preset: JUMP_HOP_STYLE_MINIMAL_BLOCKS.to_string(), + character_prompt: format!("{seed}的俯视角主角,透明背景,全身可见"), + tile_prompt: format!("{seed}的等距地块图集,包含起点、普通、目标和终点地块"), + end_mood_prompt: String::new(), + } +} + +fn apply_compile_overrides( + config: &mut JumpHopCreatorConfigSnapshot, + input: &JumpHopDraftCompileInput, +) -> Result<(), String> { + if let Some(value) = input.theme_text.as_deref().and_then(clean_optional) { + config.theme_text = value; + } + if let Some(value) = input.difficulty.as_deref().and_then(clean_optional) { + config.difficulty = value; + } + if let Some(value) = input.style_preset.as_deref().and_then(clean_optional) { + config.style_preset = value; + } + if let Some(value) = input.character_prompt.as_deref().and_then(clean_optional) { + config.character_prompt = value; + } + if let Some(value) = input.tile_prompt.as_deref().and_then(clean_optional) { + config.tile_prompt = value; + } + if let Some(value) = input.end_mood_prompt.as_deref().and_then(clean_optional) { + config.end_mood_prompt = value; + } + require_non_empty(&config.theme_text, "jump_hop theme_text")?; + require_non_empty(&config.character_prompt, "jump_hop character_prompt")?; + require_non_empty(&config.tile_prompt, "jump_hop tile_prompt")?; + Ok(()) +} + +fn require_non_empty(value: &str, label: &str) -> Result<(), String> { + if value.trim().is_empty() { + Err(format!("{label} 不能为空")) + } else { + Ok(()) + } +} + +fn clean_optional(value: &str) -> Option { + let value = value.trim(); + if value.is_empty() { + None + } else { + Some(value.to_string()) + } +} + +fn clean_string(value: &str, fallback: &str) -> String { + clean_optional(value).unwrap_or_else(|| fallback.to_string()) +} + +fn parse_config(value: &str) -> Result { + parse_json(value) +} + +fn parse_tags(value: &str) -> Result, String> { + Ok(parse_json_or_default::>(value) + .into_iter() + .map(|tag| tag.trim().to_string()) + .filter(|tag| !tag.is_empty()) + .take(8) + .collect()) +} + +fn parse_json(value: &str) -> Result +where + T: DeserializeOwned, +{ + serde_json::from_str(value).map_err(|error| error.to_string()) +} + +fn parse_json_or_default(value: &str) -> T +where + T: DeserializeOwned + Default, +{ + serde_json::from_str(value).unwrap_or_default() +} + +fn to_json_string(value: &T) -> String +where + T: Serialize, +{ + serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string()) +} + +fn session_result(session: JumpHopAgentSessionSnapshot) -> JumpHopAgentSessionProcedureResult { + JumpHopAgentSessionProcedureResult { + ok: true, + session: Some(session), + error_message: None, + } +} + +fn session_error(message: String) -> JumpHopAgentSessionProcedureResult { + JumpHopAgentSessionProcedureResult { + ok: false, + session: None, + error_message: Some(message), + } +} + +fn work_result(work: JumpHopWorkSnapshot) -> JumpHopWorkProcedureResult { + JumpHopWorkProcedureResult { + ok: true, + work: Some(work), + error_message: None, + } +} + +fn work_error(message: String) -> JumpHopWorkProcedureResult { + JumpHopWorkProcedureResult { + ok: false, + work: None, + error_message: Some(message), + } +} + +fn run_result(run: JumpHopRunSnapshot) -> JumpHopRunProcedureResult { + JumpHopRunProcedureResult { + ok: true, + run: Some(run), + error_message: None, + } +} + +fn run_error(message: String) -> JumpHopRunProcedureResult { + JumpHopRunProcedureResult { + ok: false, + run: None, + error_message: Some(message), + } +} + +fn clone_session(row: &JumpHopAgentSessionRow) -> JumpHopAgentSessionRow { + JumpHopAgentSessionRow { + session_id: row.session_id.clone(), + owner_user_id: row.owner_user_id.clone(), + seed_text: row.seed_text.clone(), + current_turn: row.current_turn, + progress_percent: row.progress_percent, + stage: row.stage.clone(), + config_json: row.config_json.clone(), + draft_json: row.draft_json.clone(), + last_assistant_reply: row.last_assistant_reply.clone(), + published_profile_id: row.published_profile_id.clone(), + created_at: row.created_at, + updated_at: row.updated_at, + } +} + +fn clone_work(row: &JumpHopWorkProfileRow) -> JumpHopWorkProfileRow { + JumpHopWorkProfileRow { + profile_id: row.profile_id.clone(), + work_id: row.work_id.clone(), + owner_user_id: row.owner_user_id.clone(), + source_session_id: row.source_session_id.clone(), + author_display_name: row.author_display_name.clone(), + work_title: row.work_title.clone(), + work_description: row.work_description.clone(), + theme_tags_json: row.theme_tags_json.clone(), + difficulty: row.difficulty.clone(), + style_preset: row.style_preset.clone(), + character_prompt: row.character_prompt.clone(), + tile_prompt: row.tile_prompt.clone(), + end_mood_prompt: row.end_mood_prompt.clone(), + character_asset_json: row.character_asset_json.clone(), + tile_atlas_asset_json: row.tile_atlas_asset_json.clone(), + tile_assets_json: row.tile_assets_json.clone(), + path_json: row.path_json.clone(), + cover_image_src: row.cover_image_src.clone(), + cover_composite: row.cover_composite.clone(), + generation_status: row.generation_status.clone(), + publication_status: row.publication_status.clone(), + play_count: row.play_count, + updated_at: row.updated_at, + published_at: row.published_at, + } +} + +fn clone_run(row: &JumpHopRuntimeRunRow) -> JumpHopRuntimeRunRow { + JumpHopRuntimeRunRow { + run_id: row.run_id.clone(), + owner_user_id: row.owner_user_id.clone(), + profile_id: row.profile_id.clone(), + status: row.status.clone(), + started_at_ms: row.started_at_ms, + finished_at_ms: row.finished_at_ms, + current_platform_index: row.current_platform_index, + score: row.score, + combo: row.combo, + snapshot_json: row.snapshot_json.clone(), + created_at: row.created_at, + updated_at: row.updated_at, + } +} diff --git a/server-rs/crates/spacetime-module/src/jump_hop/tables.rs b/server-rs/crates/spacetime-module/src/jump_hop/tables.rs new file mode 100644 index 00000000..74ef94d6 --- /dev/null +++ b/server-rs/crates/spacetime-module/src/jump_hop/tables.rs @@ -0,0 +1,91 @@ +use crate::*; + +#[spacetimedb::table( + accessor = jump_hop_agent_session, + index(accessor = by_jump_hop_agent_session_owner_user_id, btree(columns = [owner_user_id])) +)] +pub struct JumpHopAgentSessionRow { + #[primary_key] + pub(crate) session_id: String, + pub(crate) owner_user_id: String, + pub(crate) seed_text: String, + pub(crate) current_turn: u32, + pub(crate) progress_percent: u32, + pub(crate) stage: String, + pub(crate) config_json: String, + pub(crate) draft_json: String, + pub(crate) last_assistant_reply: String, + pub(crate) published_profile_id: String, + pub(crate) created_at: Timestamp, + pub(crate) updated_at: Timestamp, +} + +#[spacetimedb::table( + accessor = jump_hop_work_profile, + index(accessor = by_jump_hop_work_owner_user_id, btree(columns = [owner_user_id])), + index(accessor = by_jump_hop_work_publication_status, btree(columns = [publication_status])) +)] +pub struct JumpHopWorkProfileRow { + #[primary_key] + pub(crate) profile_id: String, + pub(crate) work_id: String, + pub(crate) owner_user_id: String, + pub(crate) source_session_id: String, + pub(crate) author_display_name: String, + pub(crate) work_title: String, + pub(crate) work_description: String, + pub(crate) theme_tags_json: String, + pub(crate) difficulty: String, + pub(crate) style_preset: String, + pub(crate) character_prompt: String, + pub(crate) tile_prompt: String, + pub(crate) end_mood_prompt: String, + pub(crate) character_asset_json: String, + pub(crate) tile_atlas_asset_json: String, + pub(crate) tile_assets_json: String, + pub(crate) path_json: String, + pub(crate) cover_image_src: String, + pub(crate) cover_composite: String, + pub(crate) generation_status: String, + pub(crate) publication_status: String, + pub(crate) play_count: u32, + pub(crate) updated_at: Timestamp, + pub(crate) published_at: Option, +} + +#[spacetimedb::table( + accessor = jump_hop_runtime_run, + index(accessor = by_jump_hop_run_owner_user_id, btree(columns = [owner_user_id])), + index(accessor = by_jump_hop_run_profile_id, btree(columns = [profile_id])) +)] +pub struct JumpHopRuntimeRunRow { + #[primary_key] + pub(crate) run_id: String, + pub(crate) owner_user_id: String, + pub(crate) profile_id: String, + pub(crate) status: String, + pub(crate) started_at_ms: i64, + pub(crate) finished_at_ms: i64, + pub(crate) current_platform_index: u32, + pub(crate) score: u32, + pub(crate) combo: u32, + pub(crate) snapshot_json: String, + pub(crate) created_at: Timestamp, + pub(crate) updated_at: Timestamp, +} + +#[spacetimedb::table( + accessor = jump_hop_event, + index(accessor = by_jump_hop_event_profile_id, btree(columns = [profile_id])), + index(accessor = by_jump_hop_event_run_id, btree(columns = [run_id])) +)] +pub struct JumpHopEventRow { + #[primary_key] + pub(crate) event_id: String, + pub(crate) owner_user_id: String, + pub(crate) profile_id: String, + pub(crate) run_id: String, + pub(crate) event_type: String, + pub(crate) result: String, + pub(crate) occurred_at: Timestamp, +} diff --git a/server-rs/crates/spacetime-module/src/jump_hop/types.rs b/server-rs/crates/spacetime-module/src/jump_hop/types.rs new file mode 100644 index 00000000..fe514a3d --- /dev/null +++ b/server-rs/crates/spacetime-module/src/jump_hop/types.rs @@ -0,0 +1,261 @@ +use crate::*; +use serde::{Deserialize, Serialize}; + +pub const JUMP_HOP_TEMPLATE_ID: &str = "jump-hop"; +pub const JUMP_HOP_TEMPLATE_NAME: &str = "跳一跳"; +pub const JUMP_HOP_STYLE_MINIMAL_BLOCKS: &str = "minimal-blocks"; +pub const JUMP_HOP_STAGE_COLLECTING: &str = "Collecting"; +pub const JUMP_HOP_STAGE_DRAFT_COMPILED: &str = "DraftCompiled"; +pub const JUMP_HOP_STAGE_PUBLISHED: &str = "Published"; +pub const JUMP_HOP_PUBLICATION_DRAFT: &str = "Draft"; +pub const JUMP_HOP_PUBLICATION_PUBLISHED: &str = "Published"; +pub const JUMP_HOP_GENERATION_DRAFT: &str = "draft"; +pub const JUMP_HOP_GENERATION_READY: &str = "ready"; +pub const JUMP_HOP_EVENT_RUN_STARTED: &str = "run-started"; +pub const JUMP_HOP_EVENT_RUN_RESTARTED: &str = "run-restarted"; +pub const JUMP_HOP_EVENT_JUMP: &str = "jump"; + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct JumpHopAgentSessionCreateInput { + pub session_id: String, + pub owner_user_id: String, + pub seed_text: String, + pub work_title: String, + pub work_description: String, + pub theme_tags_json: Option, + pub welcome_message_text: String, + pub config_json: Option, + pub created_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct JumpHopAgentSessionGetInput { + pub session_id: String, + pub owner_user_id: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct JumpHopDraftCompileInput { + pub session_id: String, + pub owner_user_id: String, + pub profile_id: String, + pub author_display_name: String, + pub seed_text: String, + pub work_title: String, + pub work_description: String, + pub theme_tags_json: Option, + pub theme_text: Option, + pub difficulty: Option, + pub style_preset: Option, + pub character_prompt: Option, + pub tile_prompt: Option, + pub end_mood_prompt: Option, + pub character_asset_json: Option, + pub tile_atlas_asset_json: Option, + pub tile_assets_json: Option, + pub cover_composite: Option, + pub generation_status: Option, + pub compiled_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct JumpHopWorkUpdateInput { + pub profile_id: String, + pub owner_user_id: String, + pub work_title: String, + pub work_description: String, + pub theme_tags_json: String, + pub difficulty: Option, + pub style_preset: Option, + pub cover_image_src: Option, + pub cover_composite: Option, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct JumpHopWorkPublishInput { + pub profile_id: String, + pub owner_user_id: String, + pub published_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct JumpHopWorksListInput { + pub owner_user_id: String, + pub published_only: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct JumpHopWorkGetInput { + pub profile_id: String, + pub owner_user_id: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct JumpHopRunStartInput { + pub run_id: String, + pub owner_user_id: String, + pub profile_id: String, + pub client_event_id: String, + pub started_at_ms: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct JumpHopRunGetInput { + pub run_id: String, + pub owner_user_id: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct JumpHopRunJumpInput { + pub run_id: String, + pub owner_user_id: String, + pub charge_ms: u32, + pub client_event_id: String, + pub jumped_at_ms: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct JumpHopRunRestartInput { + pub source_run_id: String, + pub next_run_id: String, + pub owner_user_id: String, + pub client_action_id: String, + pub restarted_at_ms: i64, +} + +#[derive(Clone, Debug, PartialEq, SpacetimeType)] +pub struct JumpHopAgentSessionProcedureResult { + pub ok: bool, + pub session: Option, + pub error_message: Option, +} + +#[derive(Clone, Debug, PartialEq, SpacetimeType)] +pub struct JumpHopWorkProcedureResult { + pub ok: bool, + pub work: Option, + pub error_message: Option, +} + +#[derive(Clone, Debug, PartialEq, SpacetimeType)] +pub struct JumpHopWorksProcedureResult { + pub ok: bool, + pub items: Vec, + pub error_message: Option, +} + +#[derive(Clone, Debug, PartialEq, SpacetimeType)] +pub struct JumpHopRunProcedureResult { + pub ok: bool, + pub run: Option, + pub error_message: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopCreatorConfigSnapshot { + pub theme_text: String, + pub difficulty: String, + pub style_preset: String, + pub character_prompt: String, + pub tile_prompt: String, + #[serde(default)] + pub end_mood_prompt: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopCharacterAssetSnapshot { + pub asset_id: String, + pub image_src: String, + pub image_object_key: String, + pub asset_object_id: String, + pub generation_provider: String, + pub prompt: String, + pub width: u32, + pub height: u32, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SpacetimeType)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopTileAssetSnapshot { + pub tile_type: String, + pub image_src: String, + pub image_object_key: String, + pub asset_object_id: String, + pub source_atlas_cell: String, + pub visual_width: u32, + pub visual_height: u32, + pub top_surface_radius: f32, + pub landing_radius: f32, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SpacetimeType)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopDraftSnapshot { + pub template_id: String, + pub template_name: String, + pub profile_id: Option, + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + pub difficulty: String, + pub style_preset: String, + pub character_prompt: String, + pub tile_prompt: String, + pub end_mood_prompt: Option, + pub character_asset: Option, + pub tile_atlas_asset: Option, + pub tile_assets: Vec, + pub path: Option, + pub cover_composite: Option, + pub generation_status: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SpacetimeType)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopAgentSessionSnapshot { + pub session_id: String, + pub owner_user_id: String, + pub seed_text: String, + pub current_turn: u32, + pub progress_percent: u32, + pub stage: String, + pub config: JumpHopCreatorConfigSnapshot, + pub draft: Option, + pub last_assistant_reply: String, + pub published_profile_id: Option, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SpacetimeType)] +#[serde(rename_all = "camelCase")] +pub struct JumpHopWorkSnapshot { + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: String, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub theme_tags: Vec, + pub difficulty: String, + pub style_preset: String, + pub character_prompt: String, + pub tile_prompt: String, + pub end_mood_prompt: Option, + pub character_asset: Option, + pub tile_atlas_asset: Option, + pub tile_assets: Vec, + pub path: module_jump_hop::JumpHopPath, + pub cover_image_src: String, + pub cover_composite: Option, + pub publication_status: String, + pub publish_ready: bool, + pub play_count: u32, + pub generation_status: String, + pub updated_at_micros: i64, + pub published_at_micros: Option, +} diff --git a/server-rs/crates/spacetime-module/src/lib.rs b/server-rs/crates/spacetime-module/src/lib.rs index 0d207981..b89390a4 100644 --- a/server-rs/crates/spacetime-module/src/lib.rs +++ b/server-rs/crates/spacetime-module/src/lib.rs @@ -8,6 +8,7 @@ pub use module_big_fish::*; pub use module_combat::*; pub use module_custom_world::*; pub use module_inventory::*; +pub use module_jump_hop::*; pub use module_npc::*; pub use module_progression::*; pub use module_quest::*; @@ -29,6 +30,7 @@ mod custom_world; mod domain_types; mod entry; mod gameplay; +mod jump_hop; mod match3d; mod migration; mod puzzle; @@ -45,6 +47,7 @@ pub use custom_world::*; pub use domain_types::*; pub use entry::*; pub use gameplay::*; +pub use jump_hop::*; pub use match3d::*; pub use migration::*; pub use runtime::*; diff --git a/server-rs/crates/spacetime-module/src/migration.rs b/server-rs/crates/spacetime-module/src/migration.rs index 8d7e78c3..d12566c6 100644 --- a/server-rs/crates/spacetime-module/src/migration.rs +++ b/server-rs/crates/spacetime-module/src/migration.rs @@ -12,6 +12,9 @@ use crate::bark_battle::tables::{ bark_battle_work_stats_projection, }; use crate::big_fish::big_fish_runtime_run; +use crate::jump_hop::tables::{ + jump_hop_agent_session, jump_hop_event, jump_hop_runtime_run, jump_hop_work_profile, +}; use crate::match3d::tables::{ match3d_agent_message, match3d_agent_session, match3d_runtime_run, match3d_work_profile, }; @@ -234,6 +237,10 @@ macro_rules! migration_tables { match3d_agent_message, match3d_work_profile, match3d_runtime_run, + jump_hop_agent_session, + jump_hop_work_profile, + jump_hop_runtime_run, + jump_hop_event, square_hole_agent_session, square_hole_agent_message, square_hole_work_profile, diff --git a/server-rs/crates/spacetime-module/src/puzzle.rs b/server-rs/crates/spacetime-module/src/puzzle.rs index 2703a355..a22ed976 100644 --- a/server-rs/crates/spacetime-module/src/puzzle.rs +++ b/server-rs/crates/spacetime-module/src/puzzle.rs @@ -1293,6 +1293,12 @@ fn select_puzzle_cover_image_tx( ui_background_prompt: target_level.ui_background_prompt, ui_background_image_src: target_level.ui_background_image_src, ui_background_image_object_key: target_level.ui_background_image_object_key, + level_scene_image_src: target_level.level_scene_image_src, + level_scene_image_object_key: target_level.level_scene_image_object_key, + ui_spritesheet_image_src: target_level.ui_spritesheet_image_src, + ui_spritesheet_image_object_key: target_level.ui_spritesheet_image_object_key, + level_background_image_src: target_level.level_background_image_src, + level_background_image_object_key: target_level.level_background_image_object_key, background_music: target_level.background_music, candidates: selected_level_draft.candidates, selected_candidate_id: selected_level_draft.selected_candidate_id, @@ -2636,6 +2642,12 @@ fn build_profile_levels_from_row( ui_background_prompt: None, ui_background_image_src: None, ui_background_image_object_key: None, + level_scene_image_src: None, + level_scene_image_object_key: None, + ui_spritesheet_image_src: None, + ui_spritesheet_image_object_key: None, + level_background_image_src: None, + level_background_image_object_key: None, background_music: None, candidates: Vec::new(), selected_candidate_id: None, diff --git a/server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs b/server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs index e4435dbb..71384fc2 100644 --- a/server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs +++ b/server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs @@ -179,6 +179,7 @@ fn seed_creation_entry_config_if_missing(ctx: &ReducerContext) { } } + migrate_rpg_entry_from_old_hidden_default(ctx, now); migrate_visual_novel_entry_from_old_visible_default(ctx, now); migrate_bark_battle_entry_to_open_default(ctx, now); migrate_coming_soon_entry_from_old_open_default( diff --git a/src/App.tsx b/src/App.tsx index 5ede559e..e16ff02f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,6 +32,16 @@ const RpgRuntimeApp = lazy(async () => { }; }); +function RuntimeLoadingFallback() { + return ( +
+
+ 正在启动 +
+
+ ); +} + function isRpgRuntimeRoute(pathname: string) { const normalizedPath = normalizeAppPath(pathname); return ( @@ -126,7 +136,7 @@ export default function App() { if (isRuntimeActive) { return ( - + }> { diff --git a/src/PuzzlePlaygroundApp.tsx b/src/PuzzlePlaygroundApp.tsx index d5794bfd..429b0761 100644 --- a/src/PuzzlePlaygroundApp.tsx +++ b/src/PuzzlePlaygroundApp.tsx @@ -22,7 +22,7 @@ const PLACEHOLDER_PUZZLE_IMAGE = - + diff --git a/src/components/CustomWorldEntityCatalog.tsx b/src/components/CustomWorldEntityCatalog.tsx index 02b7c773..005c373a 100644 --- a/src/components/CustomWorldEntityCatalog.tsx +++ b/src/components/CustomWorldEntityCatalog.tsx @@ -173,7 +173,7 @@ function ImageFrame({ }) { return (
{src ? (
@@ -313,7 +313,7 @@ function OpeningCgPreview({
{isGenerating ? (
-
+
) : null} {openingCg?.status === 'failed' && openingCg.errorMessage ? ( @@ -437,7 +437,7 @@ function CatalogCard({
@@ -453,7 +453,7 @@ function CatalogCard({ onClick={disabled ? undefined : onClick} aria-disabled={disabled} className={`w-full rounded-[1.3rem] border p-2.5 text-left transition-colors xl:p-3 ${ - isSelected ? 'border-rose-300/35 bg-rose-500/10' : 'platform-subpanel' + isSelected ? 'border-[var(--platform-button-danger-border)] bg-[var(--platform-button-danger-fill)]' : 'platform-subpanel' }`} >
@@ -491,7 +491,7 @@ function CatalogCard({ onClick={disabled ? undefined : onClick} aria-disabled={disabled} className={`w-full rounded-[1.4rem] border p-3 text-left transition-colors ${ - isSelected ? 'border-rose-300/35 bg-rose-500/10' : 'platform-subpanel' + isSelected ? 'border-[var(--platform-button-danger-border)] bg-[var(--platform-button-danger-fill)]' : 'platform-subpanel' }`} >
@@ -899,7 +899,7 @@ export function CustomWorldEntityCatalog({ ref={scrollContainerRef} className="h-full min-h-0 space-y-3 overflow-y-auto overscroll-contain pr-1 scrollbar-hide xl:space-y-4 xl:pr-2 2xl:space-y-5 2xl:pr-3" > -
+
世界档案
@@ -913,7 +913,7 @@ export function CustomWorldEntityCatalog({
-
+
{RESULT_TABS.map((tab) => (
diff --git a/src/components/CustomWorldGenerationView.tsx b/src/components/CustomWorldGenerationView.tsx index 70ff15d7..b540a4f0 100644 --- a/src/components/CustomWorldGenerationView.tsx +++ b/src/components/CustomWorldGenerationView.tsx @@ -225,7 +225,7 @@ export function CustomWorldGenerationView({
@@ -283,9 +283,9 @@ export function CustomWorldGenerationView({ )} className={`rounded-2xl border px-4 py-3 transition-colors ${ step.status === 'completed' - ? 'border-emerald-400/16 bg-emerald-500/8' + ? 'border-[var(--platform-success-border)] bg-[var(--platform-success-bg)]' : step.status === 'active' - ? 'border-sky-300/22 bg-sky-500/10' + ? 'border-[var(--platform-cool-border)] bg-[var(--platform-cool-bg)]' : 'platform-subpanel' } custom-world-generation-step`} initial={ @@ -317,9 +317,9 @@ export function CustomWorldGenerationView({ {error ? ( -
+
{error}
) : null} @@ -364,7 +364,7 @@ export function CustomWorldGenerationView({ diff --git a/src/components/CustomWorldResultView.test.tsx b/src/components/CustomWorldResultView.test.tsx index 2142efe8..cf381a5f 100644 --- a/src/components/CustomWorldResultView.test.tsx +++ b/src/components/CustomWorldResultView.test.tsx @@ -5,6 +5,7 @@ import userEvent from '@testing-library/user-event'; import { useState } from 'react'; import { expect, test, vi } from 'vitest'; +import { normalizeCustomWorldProfileRecord } from '../data/customWorldLibrary'; import * as rpgCreationAssetClient from '../services/rpg-creation/rpgCreationAssetClient'; import type { CustomWorldPlayableNpc, CustomWorldProfile } from '../types'; import { RpgCreationResultView } from './rpg-creation-result/RpgCreationResultView'; @@ -286,6 +287,40 @@ function ResultViewHarness() { ); } +function ResultViewRehydratingHarness() { + const [profile, setProfile] = useState(baseProfile); + const [rehydrated, setRehydrated] = useState(false); + + return ( +
+
{rehydrated ? 'yes' : 'no'}
+ {}} + onProfileChange={(nextProfile) => { + setProfile(nextProfile); + if (!nextProfile.openingCg) { + return; + } + + window.setTimeout(() => { + const normalized = normalizeCustomWorldProfileRecord(nextProfile); + if (normalized) { + setProfile(normalized); + } + setRehydrated(true); + }, 0); + }} + /> +
+ ); +} + test('clicking新增可扮演角色 shows pending item, disables button, and marks result as new', async () => { const user = userEvent.setup(); @@ -385,6 +420,40 @@ test('world tab generates opening cg only after manual click and writes it back }); }); +test('world tab keeps opening cg visible after parent rehydrates normalized profile', async () => { + const user = userEvent.setup(); + mockedRpgCreationAssetClient.generateOpeningCg.mockResolvedValue({ + id: 'opening-cg-1', + status: 'ready', + storyboardImageSrc: '/generated-custom-world-scenes/world/opening/storyboard.png', + storyboardAssetId: 'storyboard-1', + videoSrc: '/generated-custom-world-scenes/world/opening/opening.mp4', + videoAssetId: 'video-1', + imageModel: 'gpt-image-2', + videoModel: 'doubao-seedance-2-0-fast-260128', + aspectRatio: '16:9', + imageSize: '2k', + videoResolution: '480p', + durationSeconds: 15, + pointCost: 80, + estimatedWaitMinutes: 10, + updatedAt: '2026-05-03T00:00:00Z', + }); + + render(); + + await user.click(screen.getByRole('button', { name: '生成' })); + + await waitFor(() => { + expect(screen.getByTestId('rehydrated').textContent).toBe('yes'); + }); + expect( + document.querySelector( + 'video[src="/generated-custom-world-scenes/world/opening/opening.mp4"]', + ), + ).toBeTruthy(); +}); + test('playable tab prefers generated portrait over runtime preview placeholder', async () => { const user = userEvent.setup(); const profile = { diff --git a/src/components/auth/AccountModal.tsx b/src/components/auth/AccountModal.tsx index 6c346beb..45de37cb 100644 --- a/src/components/auth/AccountModal.tsx +++ b/src/components/auth/AccountModal.tsx @@ -260,7 +260,7 @@ function ThemeOptionCard({ onClick={onClick} className={`platform-subpanel w-full rounded-[1.5rem] p-4 text-left transition ${ active - ? 'border-[var(--platform-surface-hover-border)] shadow-[0_18px_44px_rgba(255,91,132,0.14)]' + ? 'border-[var(--platform-surface-hover-border)] shadow-[0_18px_44px_rgba(112,57,30,0.14)]' : 'hover:border-[var(--platform-surface-hover-border)]' }`} > @@ -534,8 +534,8 @@ export function AccountModal({ onPlatformThemeChange('light')} />
-
+
陶泥儿
视觉叙事 RPG
diff --git a/src/components/bark-battle-creation/BarkBattleConfigEditor.tsx b/src/components/bark-battle-creation/BarkBattleConfigEditor.tsx index 87adbdb4..42246dcd 100644 --- a/src/components/bark-battle-creation/BarkBattleConfigEditor.tsx +++ b/src/components/bark-battle-creation/BarkBattleConfigEditor.tsx @@ -181,7 +181,7 @@ export function BarkBattleConfigEditor({ value={title} disabled={isBusy} onChange={(event) => setTitle(event.target.value)} - className="h-11 w-full rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 text-base font-semibold text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100" + className="h-11 w-full rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 text-base font-semibold text-[var(--platform-text-strong)] outline-none transition focus:border-[var(--platform-surface-hover-border)] focus:bg-white focus:ring-2 focus:ring-[var(--platform-warm-border)]" maxLength={40} aria-label="作品标题" /> @@ -193,7 +193,7 @@ export function BarkBattleConfigEditor({ value={description} disabled={isBusy} onChange={(event) => setDescription(event.target.value)} - className="h-[5.5rem] min-h-[5.5rem] w-full resize-none rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-base leading-6 text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100" + className="h-[5.5rem] min-h-[5.5rem] w-full resize-none rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-base leading-6 text-[var(--platform-text-strong)] outline-none transition focus:border-[var(--platform-surface-hover-border)] focus:bg-white focus:ring-2 focus:ring-[var(--platform-warm-border)]" maxLength={160} placeholder="" aria-label="简介" @@ -211,7 +211,7 @@ export function BarkBattleConfigEditor({ event.target.value as BarkBattleDifficultyPreset, ) } - className="h-11 w-full rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 text-sm font-black text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100" + className="h-11 w-full rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 text-sm font-black text-[var(--platform-text-strong)] outline-none transition focus:border-[var(--platform-surface-hover-border)] focus:bg-white focus:ring-2 focus:ring-[var(--platform-warm-border)]" aria-label="难度预设" > {DIFFICULTY_OPTIONS.map((option) => ( diff --git a/src/components/common/CreativeImageInputPanel.test.tsx b/src/components/common/CreativeImageInputPanel.test.tsx index 11c175e1..22532783 100644 --- a/src/components/common/CreativeImageInputPanel.test.tsx +++ b/src/components/common/CreativeImageInputPanel.test.tsx @@ -143,3 +143,196 @@ test('creative image input panel confirms before removing uploaded image', () => fireEvent.click(within(dialog).getByRole('button', { name: '移除' })); expect(onMainImageRemove).toHaveBeenCalledTimes(1); }); + +test('creative image input panel supports a preview-only main image mode', () => { + const onSubmit = vi.fn(); + + render( + {}} + onMainImageRemove={() => {}} + onAiRedrawChange={() => {}} + onPromptChange={() => {}} + onSubmit={onSubmit} + />, + ); + + expect(screen.getByAltText('UI背景预览').getAttribute('src')).toBe( + '/generated-puzzle-assets/session/ui/background.png', + ); + expect(screen.queryByLabelText('上传拼图图片')).toBeNull(); + expect(screen.queryByRole('button', { name: '选择历史图片' })).toBeNull(); + expect(screen.queryByRole('switch', { name: 'AI重绘' })).toBeNull(); + expect(screen.getByLabelText('UI背景提示词')).toHaveProperty( + 'value', + '雨夜猫街竖屏拼图UI背景', + ); + + fireEvent.click(screen.getByRole('button', { name: '生成UI背景' })); + expect(onSubmit).toHaveBeenCalledTimes(1); +}); + +test('creative image input panel does not show empty upload hint over a non-removable image', () => { + render( + {}} + onMainImageRemove={() => {}} + onAiRedrawChange={() => {}} + onPromptChange={() => {}} + onSubmit={() => {}} + />, + ); + + expect(screen.getByAltText('拼图关卡图')).toBeTruthy(); + expect(screen.queryByText('上传图片/填写画面描述')).toBeNull(); + expect(screen.queryByRole('button', { name: '移除参考图' })).toBeNull(); +}); + +test('creative image input panel can show an image without exposing AI redraw controls', () => { + render( + {}} + onMainImageRemove={() => {}} + onAiRedrawChange={() => {}} + onPromptChange={() => {}} + onSubmit={() => {}} + />, + ); + + expect(screen.getByAltText('拼图关卡图')).toBeTruthy(); + expect(screen.queryByRole('switch', { name: 'AI重绘' })).toBeNull(); + expect(screen.getByLabelText('画面描述')).toBeTruthy(); +}); + +test('creative image input panel can upload prompt references while showing a main image', () => { + const onPromptReferenceFilesSelect = vi.fn(); + + render( + {}} + onMainImageRemove={() => {}} + onAiRedrawChange={() => {}} + onPromptChange={() => {}} + onPromptReferenceFilesSelect={onPromptReferenceFilesSelect} + onSubmit={() => {}} + />, + ); + + const promptReferenceInput = screen.getByLabelText('上传参考图', { + selector: 'input', + }); + fireEvent.change(promptReferenceInput, { + target: { + files: [new File(['a'], 'prompt-reference.png', { type: 'image/png' })], + }, + }); + + expect(onPromptReferenceFilesSelect).toHaveBeenCalledWith([ + expect.any(File), + ]); + expect( + screen.getByRole('button', { name: '预览参考图 描述参考图 1' }), + ).toBeTruthy(); +}); diff --git a/src/components/common/CreativeImageInputPanel.tsx b/src/components/common/CreativeImageInputPanel.tsx index 90a7a431..78448e9d 100644 --- a/src/components/common/CreativeImageInputPanel.tsx +++ b/src/components/common/CreativeImageInputPanel.tsx @@ -14,6 +14,7 @@ export type CreativeImageInputReferenceImage = { id: string; label: string; imageSrc: string; + assetObjectId?: string | null; }; export type CreativeImageInputPanelLabels = { @@ -34,13 +35,20 @@ export type CreativeImageInputPanelProps = { className?: string; disabled?: boolean; isSubmitting?: boolean; + mainImageMode?: 'edit' | 'preview'; + canRemoveMainImage?: boolean; + canToggleAiRedraw?: boolean; + canUploadPromptReferences?: boolean; uploadedImageSrc: string; uploadedImageAlt: string; + uploadedImageRefreshKey?: string | number | null; + mainImageMeta?: ReactNode; mainImageInputId: string; mainImageAccept?: string; promptTextareaId: string; prompt: string; promptLabel: string; + promptAriaLabel?: string; promptRows?: number; aiRedraw: boolean; promptReferenceImages: CreativeImageInputReferenceImage[]; @@ -69,13 +77,20 @@ export function CreativeImageInputPanel({ className = '', disabled = false, isSubmitting = false, + mainImageMode = 'edit', + canRemoveMainImage = true, + canToggleAiRedraw = true, + canUploadPromptReferences, uploadedImageSrc, uploadedImageAlt, + uploadedImageRefreshKey = null, + mainImageMeta = null, mainImageInputId, mainImageAccept = DEFAULT_IMAGE_ACCEPT, promptTextareaId, prompt, promptLabel, + promptAriaLabel, promptRows = 2, aiRedraw, promptReferenceImages, @@ -100,9 +115,12 @@ export function CreativeImageInputPanel({ useState(null); const [isRemoveImageConfirmOpen, setIsRemoveImageConfirmOpen] = useState(false); - const showPrompt = !uploadedImageSrc || aiRedraw; + const showPrompt = mainImageMode === 'preview' || !uploadedImageSrc || aiRedraw; + const shouldShowPromptReferences = + canUploadPromptReferences ?? !uploadedImageSrc; const promptReferenceUploadDisabled = disabled || promptReferenceImages.length >= promptReferenceLimit; + const canEditMainImage = mainImageMode === 'edit'; useEffect(() => { if (uploadedImageSrc) { @@ -143,34 +161,41 @@ export function CreativeImageInputPanel({ {labels.imageField}
-
- { - const file = event.currentTarget.files?.[0] ?? null; - event.currentTarget.value = ''; - if (file) { - onMainImageFileSelect(file); - } - }} - className="sr-only" - /> - +
+ {canEditMainImage ? ( + <> + { + const file = event.currentTarget.files?.[0] ?? null; + event.currentTarget.value = ''; + if (file) { + onMainImageFileSelect(file); + } + }} + className="sr-only" + /> + + + ) : null} {uploadedImageSrc ? ( @@ -182,12 +207,12 @@ export function CreativeImageInputPanel({ )}
- {onHistoryClick ? ( + {canEditMainImage && onHistoryClick ? ( ) : null} - {uploadedImageSrc ? ( + {canEditMainImage && uploadedImageSrc && canToggleAiRedraw ? ( ) : null} - {uploadedImageSrc ? ( + {canEditMainImage && uploadedImageSrc && canRemoveMainImage ? ( - ) : ( + ) : canEditMainImage && !uploadedImageSrc ? ( - )} + ) : null}
+ {mainImageMeta ?
{mainImageMeta}
: null}
{showPrompt ? ( @@ -267,12 +293,12 @@ export function CreativeImageInputPanel({ placeholder="" onChange={(event) => onPromptChange(event.target.value)} className="h-[6rem] min-h-[6rem] w-full resize-none rounded-[1.15rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 pb-14 text-base leading-6 text-[var(--platform-text-strong)] outline-none placeholder:text-zinc-400 sm:h-[7.5rem] sm:min-h-[7.5rem] lg:h-[9.25rem] lg:min-h-[9.25rem]" - aria-label={promptLabel} + aria-label={promptAriaLabel ?? promptLabel} /> {imageModelPicker} - {!uploadedImageSrc && onPromptReferenceFilesSelect ? ( + {shouldShowPromptReferences && onPromptReferenceFilesSelect ? (
- {!uploadedImageSrc && promptReferenceImages.length > 0 ? ( + {shouldShowPromptReferences && promptReferenceImages.length > 0 ? (
{promptReferenceImages.map((reference) => (
onPromptReferenceRemove(reference.id)} - className="absolute right-0.5 top-0.5 inline-flex h-5 w-5 items-center justify-center rounded-full bg-white/94 text-[var(--platform-text-strong)] shadow-sm transition hover:text-[#ff4056] disabled:opacity-55" + className="absolute right-0.5 top-0.5 inline-flex h-5 w-5 items-center justify-center rounded-full bg-white/94 text-[var(--platform-text-strong)] shadow-sm transition hover:text-[var(--platform-accent)] disabled:opacity-55" aria-label={`移除参考图 ${reference.label}`} title="移除参考图" > diff --git a/src/components/common/SquareImageCropModal.tsx b/src/components/common/SquareImageCropModal.tsx index 50dce5f1..35c509a1 100644 --- a/src/components/common/SquareImageCropModal.tsx +++ b/src/components/common/SquareImageCropModal.tsx @@ -377,7 +377,7 @@ export function SquareImageCropModal({ className="h-full w-full object-fill" />
- + ))}
{error ? ( -
+
{error}
) : null} diff --git a/src/components/creation-agent/CreationAgentWorkspace.tsx b/src/components/creation-agent/CreationAgentWorkspace.tsx index e2c7935b..1bfa19e1 100644 --- a/src/components/creation-agent/CreationAgentWorkspace.tsx +++ b/src/components/creation-agent/CreationAgentWorkspace.tsx @@ -136,7 +136,7 @@ function CreationAgentOperationBanner({ : 'platform-banner--success'; const progress = normalizeCreationAgentProgress(visibleOperation.progress); const progressFillStyle = isFailed - ? { background: 'linear-gradient(90deg, #fb7185 0%, #f43f5e 100%)' } + ? { background: 'linear-gradient(90deg, #c7653d 0%, #a6402f 100%)' } : isRunning ? { background: 'var(--platform-button-primary-fill)' } : { background: 'linear-gradient(90deg, #86efac 0%, #34d399 100%)' }; diff --git a/src/components/creative-agent/CreativeAgentWorkspace.tsx b/src/components/creative-agent/CreativeAgentWorkspace.tsx index 918da8cb..30d527aa 100644 --- a/src/components/creative-agent/CreativeAgentWorkspace.tsx +++ b/src/components/creative-agent/CreativeAgentWorkspace.tsx @@ -171,7 +171,7 @@ export function CreativeAgentWorkspace({ {targetBinding ? (
- +
@@ -203,7 +203,7 @@ export function CreativeAgentWorkspace({ key={message.id} className={`max-w-[86%] rounded-[1.15rem] px-4 py-3 text-sm leading-6 ${ message.role === 'user' - ? 'ml-auto bg-[var(--platform-button-primary-fill)] text-[var(--platform-button-primary-text)]' + ? 'ml-auto bg-[var(--platform-button-primary-solid)] text-[var(--platform-button-primary-text)]' : 'platform-subpanel text-[var(--platform-text-base)]' }`} > diff --git a/src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx b/src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx index aae3967a..44add0ee 100644 --- a/src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx +++ b/src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx @@ -26,6 +26,17 @@ const testEntryConfig = { description: '先选玩法类型,再进入对应创作工作台。', }, creationTypes: [ + { + id: 'rpg', + title: '文字冒险', + subtitle: '经典 RPG 体验', + badge: '可创建', + imageSrc: '/creation-type-references/rpg.webp', + visible: true, + open: true, + sortOrder: 10, + updatedAtMicros: 1, + }, { id: 'puzzle', title: '拼图', @@ -290,14 +301,18 @@ test('creation hub reflects updated draft title summary and counts after rerende const match3dButton = screen.getByRole('button', { name: /抓大鹅.*3D 消除关卡/u, }); + const rpgButton = screen.getByRole('button', { + name: /文字冒险.*经典 RPG 体验/u, + }); expect(puzzleButton).toBeTruthy(); expect(match3dButton).toBeTruthy(); + expect(rpgButton).toBeTruthy(); expect((puzzleButton as HTMLButtonElement).disabled).toBe(false); expect((match3dButton as HTMLButtonElement).disabled).toBe(false); + expect((rpgButton as HTMLButtonElement).disabled).toBe(false); expect(screen.queryByRole('button', { name: /方洞挑战/u })).toBeNull(); expect(screen.queryByText('反直觉形状分拣')).toBeNull(); expect(screen.queryByRole('button', { name: /智能创作/u })).toBeNull(); - expect(screen.queryByRole('button', { name: /文字冒险/u })).toBeNull(); expect(screen.queryByRole('button', { name: /大鱼吃小鱼/u })).toBeNull(); await user.click(match3dButton); diff --git a/src/components/custom-world-home/CustomWorldCreationHub.test.tsx b/src/components/custom-world-home/CustomWorldCreationHub.test.tsx index 8ece1b8b..ced0e82c 100644 --- a/src/components/custom-world-home/CustomWorldCreationHub.test.tsx +++ b/src/components/custom-world-home/CustomWorldCreationHub.test.tsx @@ -19,6 +19,17 @@ const testEntryConfig = { description: '先选玩法类型,再进入对应创作工作台。', }, creationTypes: [ + { + id: 'rpg', + title: '文字冒险', + subtitle: '经典 RPG 体验', + badge: '可创建', + imageSrc: '/creation-type-references/rpg.webp', + visible: true, + open: true, + sortOrder: 10, + updatedAtMicros: 1, + }, { id: 'puzzle', title: '拼图', @@ -124,7 +135,8 @@ test('creation hub draft card renders compiled work summary fields', () => { expect(html).toContain('拼图关卡创作'); expect(html).toContain('抓大鹅'); expect(html).toContain('3D 消除关卡'); - expect(html).not.toContain('文字冒险'); + expect(html).toContain('文字冒险'); + expect(html).toContain('经典 RPG 体验'); expect(html).not.toContain('大鱼吃小鱼'); }); diff --git a/src/components/game-canvas/GameCanvasEntityLayer.test.tsx b/src/components/game-canvas/GameCanvasEntityLayer.test.tsx index 6b26af15..86a1f313 100644 --- a/src/components/game-canvas/GameCanvasEntityLayer.test.tsx +++ b/src/components/game-canvas/GameCanvasEntityLayer.test.tsx @@ -7,7 +7,10 @@ import { type Encounter, type SceneHostileNpc, } from '../../types'; -import { GameCanvasEntityLayer } from './GameCanvasEntityLayer'; +import { + GameCanvasEntityLayer, + getCombatFloatingNumberPresentation, +} from './GameCanvasEntityLayer'; import { CHARACTER_COMBAT_HP_TOP_PX, ENTITY_CONTAINER_REM, @@ -125,6 +128,21 @@ function renderEntityLayer(effectNpcId: string | null) { } describe('GameCanvasEntityLayer', () => { + it('keeps combat floating numbers readable on dark noisy battle backgrounds', () => { + const damage = getCombatFloatingNumberPresentation(false); + const healing = getCombatFloatingNumberPresentation(true); + + expect(damage.toneClass).toContain('bg-rose-950/72'); + expect(damage.toneClass).toContain('text-rose-50'); + expect(damage.textStyle.WebkitTextStroke).toContain('rgba(127, 29, 29'); + expect(damage.textStyle.textShadow).toContain('rgba(0, 0, 0'); + + expect(healing.toneClass).toContain('bg-emerald-950/70'); + expect(healing.toneClass).toContain('text-emerald-50'); + expect(healing.textStyle.WebkitTextStroke).toContain('rgba(6, 78, 59'); + expect(healing.textStyle.textShadow).toContain('rgba(0, 0, 0'); + }); + it('uses mirrored stage anchors for player and opponent containers', () => { expect(getMirroredStageEntityLeft('15%', 'player')).toBe('15%'); expect(getMirroredStageEntityLeft('15%', 'opponent')).toBe(`calc(100% - 15% - ${ENTITY_CONTAINER_REM}rem)`); diff --git a/src/components/game-canvas/GameCanvasEntityLayer.tsx b/src/components/game-canvas/GameCanvasEntityLayer.tsx index f374e938..19928c93 100644 --- a/src/components/game-canvas/GameCanvasEntityLayer.tsx +++ b/src/components/game-canvas/GameCanvasEntityLayer.tsx @@ -1,5 +1,5 @@ import {motion} from 'motion/react'; -import {type ReactNode, useEffect, useMemo, useRef, useState} from 'react'; +import {type CSSProperties, type ReactNode, useEffect, useMemo, useRef, useState} from 'react'; import {getCharacterById} from '../../data/characterPresets'; import {getFacingTowardPlayer, MONSTERS_BY_WORLD} from '../../data/hostileNpcs'; @@ -130,6 +130,45 @@ function getSceneTransitionMotionConfig( }; } +export function getCombatFloatingNumberPresentation(isHealing: boolean): { + toneClass: string; + textStyle: CSSProperties; +} { + const textShadow = [ + '0 1px 0 rgba(0, 0, 0, 0.98)', + '0 0 8px rgba(0, 0, 0, 0.92)', + '0 0 16px rgba(0, 0, 0, 0.72)', + ].join(', '); + + if (isHealing) { + return { + toneClass: [ + 'border-emerald-100/70', + 'bg-emerald-950/70', + 'text-emerald-50', + 'shadow-[0_0_18px_rgba(52,211,153,0.55)]', + ].join(' '), + textStyle: { + WebkitTextStroke: '1.45px rgba(6, 78, 59, 0.95)', + textShadow, + }, + }; + } + + return { + toneClass: [ + 'border-rose-100/75', + 'bg-rose-950/72', + 'text-rose-50', + 'shadow-[0_0_20px_rgba(248,113,113,0.68)]', + ].join(' '), + textStyle: { + WebkitTextStroke: '1.55px rgba(127, 29, 29, 0.98)', + textShadow, + }, + }; +} + function CombatFloatingNumber({ event, onDone, @@ -139,23 +178,20 @@ function CombatFloatingNumber({ }) { const isHealing = event.delta > 0; const deltaText = `${isHealing ? '+' : ''}${event.delta}`; - const colorClass = isHealing ? 'text-emerald-200' : 'text-rose-200'; - const glowClass = isHealing - ? 'drop-shadow-[0_0_8px_rgba(52,211,153,0.9)]' - : 'drop-shadow-[0_0_8px_rgba(248,113,113,0.9)]'; + const presentation = getCombatFloatingNumberPresentation(isHealing); return ( onDone(event.id)} - className={`pointer-events-none absolute -top-16 left-1/2 z-[14] -translate-x-1/2 text-lg font-black leading-none ${colorClass} ${glowClass}`} + className={`pointer-events-none absolute -top-[4.65rem] left-1/2 z-[38] flex min-w-[2.4rem] -translate-x-1/2 select-none items-center justify-center rounded-full border px-1.5 py-0.5 text-[1.45rem] font-black leading-none tracking-[-0.04em] sm:text-[1.6rem] ${presentation.toneClass}`} data-testid={`combat-feedback-${event.targetKey}`} aria-label={`战斗数值 ${deltaText}`} > - + {deltaText} diff --git a/src/components/jump-hop-creation/JumpHopWorkspace.tsx b/src/components/jump-hop-creation/JumpHopWorkspace.tsx new file mode 100644 index 00000000..d5b31e63 --- /dev/null +++ b/src/components/jump-hop-creation/JumpHopWorkspace.tsx @@ -0,0 +1,278 @@ +import { ArrowLeft, Loader2, Send } from 'lucide-react'; +import { useMemo, useState } from 'react'; + +import type { + JumpHopDifficulty, + JumpHopSessionResponse, + JumpHopStylePreset, + JumpHopWorkspaceCreateRequest, +} from '../../../packages/shared/src/contracts/jumpHop'; +import { jumpHopClient } from '../../services/jump-hop/jumpHopClient'; + +type JumpHopWorkspaceProps = { + isBusy?: boolean; + error?: string | null; + onBack: () => void; + onSubmitted: ( + result: JumpHopSessionResponse, + payload: JumpHopWorkspaceCreateRequest, + ) => void; +}; + +type JumpHopWorkspaceFormState = { + workTitle: string; + workDescription: string; + themeTags: string; + difficulty: JumpHopDifficulty; + stylePreset: JumpHopStylePreset; + characterPrompt: string; + tilePrompt: string; + endMoodPrompt: string; +}; + +const DEFAULT_FORM_STATE: JumpHopWorkspaceFormState = { + workTitle: '', + workDescription: '', + themeTags: '', + difficulty: 'easy', + stylePreset: 'minimal-blocks', + characterPrompt: '', + tilePrompt: '', + endMoodPrompt: '', +}; + +export function JumpHopWorkspace({ + isBusy = false, + error = null, + onBack, + onSubmitted, +}: JumpHopWorkspaceProps) { + const [formState, setFormState] = useState(DEFAULT_FORM_STATE); + const [localError, setLocalError] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const canSubmit = useMemo( + () => + Boolean( + formState.workTitle.trim() && + formState.workDescription.trim() && + formState.themeTags.trim() && + formState.characterPrompt.trim() && + formState.tilePrompt.trim(), + ), + [formState], + ); + + const handleSubmit = async () => { + if (!canSubmit || isSubmitting || isBusy) { + setLocalError('请先补全输入。'); + return; + } + + setIsSubmitting(true); + setLocalError(null); + + try { + const payload: JumpHopWorkspaceCreateRequest = { + templateId: 'jump-hop', + workTitle: formState.workTitle.trim(), + workDescription: formState.workDescription.trim(), + themeTags: formState.themeTags + .split(/[,,、\s]+/) + .map((item) => item.trim()) + .filter(Boolean), + difficulty: formState.difficulty, + stylePreset: formState.stylePreset, + characterPrompt: formState.characterPrompt.trim(), + tilePrompt: formState.tilePrompt.trim(), + endMoodPrompt: formState.endMoodPrompt.trim() || null, + }; + const response = await jumpHopClient.createSession(payload); + onSubmitted(response, payload); + } catch (caughtError) { + setLocalError( + caughtError instanceof Error ? caughtError.message : '创建草稿失败。', + ); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+
+ +
+ +
+ +